{
  "info": {
    "name": "DocumentForge",
    "description": "REST collection for DocumentForge. Two folders: **Application** covers the CRUD / query surface your app would use; **Database Operations** covers admin + replication + sharding-support endpoints a management tool would drive.\n\n- Set the `baseUrl` variable to your node's URL (default `http://localhost:5000`).\n- Set `apiKey` if the node was started with `--api-key` or `DFDB_API_KEY`.\n- The `orderId` / `flightId` / `pnr` collection variables are filled in by the Tests scripts on the Seed / Insert requests so subsequent requests just work.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_postman_id": "dfdb-v1-collection"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      { "key": "token", "value": "{{apiKey}}", "type": "string" }
    ]
  },
  "variable": [
    { "key": "baseUrl", "value": "http://localhost:5000", "description": "URL of the dfdb serve node." },
    { "key": "apiKey", "value": "", "description": "Bearer token (only needed if --api-key was set)." },
    { "key": "orderId", "value": "", "description": "Auto-filled by the Insert-order request." },
    { "key": "flightId", "value": "", "description": "Auto-filled by the Insert-flight request." },
    { "key": "pnr", "value": "DEMO01", "description": "A PNR used across examples." },
    { "key": "replicationPort", "value": "5500" },
    { "key": "leaderHost", "value": "localhost" },
    { "key": "leaderPort", "value": "5500" }
  ],
  "item": [
    {
      "name": "Application",
      "description": "End-to-end examples of how an application (the airline reservation demo) would use the database: seed data, query it, insert/find/delete individual documents.",
      "item": [
        {
          "name": "Health check",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], "path": ["health"] },
            "description": "Liveness + identity. Returns node name, version, read-only flag, and uptime."
          }
        },
        {
          "name": "Seed sample airline data",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Seed succeeded', () => pm.expect(pm.response.json().success).to.be.true);"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{ \"orders\": 500 }" },
            "url": { "raw": "{{baseUrl}}/seed", "host": ["{{baseUrl}}"], "path": ["seed"] },
            "description": "Populates `orders` and `flights` collections with realistic airline data and creates 5 indexes."
          }
        },
        {
          "name": "List collections",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/collections", "host": ["{{baseUrl}}"], "path": ["collections"] }
          }
        },
        {
          "name": "Database stats",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/stats", "host": ["{{baseUrl}}"], "path": ["stats"] },
            "description": "File size, page count, cache status, per-collection document counts, and all indexes."
          }
        },
        {
          "name": "Insert an order",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "const r = pm.response.json();",
                  "if (r && r.id) pm.collectionVariables.set('orderId', r.id);"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"pnr\": \"{{pnr}}\",\n  \"status\": \"CONFIRMED\",\n  \"passenger\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Smith\"\n  },\n  \"flights\": [\n    {\n      \"flightNumber\": \"AA100\",\n      \"departureAirport\": \"JFK\",\n      \"arrivalAirport\": \"LHR\",\n      \"fareAmount\": 650\n    }\n  ]\n}"
            },
            "url": { "raw": "{{baseUrl}}/collections/orders", "host": ["{{baseUrl}}"], "path": ["collections", "orders"] },
            "description": "Insert a single JSON document. The response's `id` is saved to {{orderId}}."
          }
        },
        {
          "name": "Bulk insert flights",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "[\n  { \"flightNumber\": \"AA200\", \"departureAirport\": \"LAX\", \"arrivalAirport\": \"JFK\" },\n  { \"flightNumber\": \"UA300\", \"departureAirport\": \"ORD\", \"arrivalAirport\": \"SFO\" },\n  { \"flightNumber\": \"DL400\", \"departureAirport\": \"ATL\", \"arrivalAirport\": \"SEA\" }\n]"
            },
            "url": { "raw": "{{baseUrl}}/collections/flights/bulk", "host": ["{{baseUrl}}"], "path": ["collections", "flights", "bulk"] },
            "description": "Insert many documents in one call. Accepts a JSON array. Single write lock taken once across the whole batch — expect 50K+ docs/sec.\n\nResponse includes `indexesRebuilt`: by default, all indexes on the collection are rebuilt after the bulk insert so subsequent indexed queries see the new rows. For cold-load workloads where you'd rather rebuild once at the end, see the next request."
          }
        },
        {
          "name": "Bulk insert (skip indexes for cold load)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "[\n  { \"flightNumber\": \"BA500\", \"departureAirport\": \"LHR\", \"arrivalAirport\": \"JFK\" },\n  { \"flightNumber\": \"LH600\", \"departureAirport\": \"FRA\", \"arrivalAirport\": \"EWR\" }\n]"
            },
            "url": {
              "raw": "{{baseUrl}}/collections/flights/bulk?skipIndexes=true",
              "host": ["{{baseUrl}}"],
              "path": ["collections", "flights", "bulk"],
              "query": [{ "key": "skipIndexes", "value": "true" }]
            },
            "description": "Power-user mode for ingest pipelines. Inserts the rows but DOES NOT rebuild indexes — you MUST call POST /admin/rebuild-indexes/{collection} (Database Operations → Admin) before any indexed query, or you'll silently miss rows.\n\nUseful when chaining many bulk batches: skip on each batch, rebuild once at the end."
          }
        },
        {
          "name": "List documents in a collection",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/collections/orders?limit=5",
              "host": ["{{baseUrl}}"],
              "path": ["collections", "orders"],
              "query": [{ "key": "limit", "value": "5" }]
            }
          }
        },
        {
          "name": "Find by business key (by PNR)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/collections/orders/by/pnr/{{pnr}}",
              "host": ["{{baseUrl}}"],
              "path": ["collections", "orders", "by", "pnr", "{{pnr}}"]
            },
            "description": "Look up by your own logical key (PNR, email, SKU, …) — uses an index when one exists, falls back to collection scan otherwise. The natural endpoint for app code; you do NOT need to track DocumentForge's internal `_id` for this.\n\nField name accepts dot/bracket paths, e.g. `passenger.lastName` or `flights[0].departureAirport`."
          }
        },
        {
          "name": "Find by nested path (lastName)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/collections/orders/by/passenger.lastName/Smith",
              "host": ["{{baseUrl}}"],
              "path": ["collections", "orders", "by", "passenger.lastName", "Smith"]
            },
            "description": "Same /by/ pattern but with a nested-path field name. If `idx_orders_lastname` exists it's used (sub-millisecond)."
          }
        },
        {
          "name": "Delete by business key (by PNR)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/collections/orders/by/pnr/{{pnr}}",
              "host": ["{{baseUrl}}"],
              "path": ["collections", "orders", "by", "pnr", "{{pnr}}"]
            },
            "description": "Symmetric delete by logical key. Returns `deletedCount` and the chosen execution plan."
          }
        },
        {
          "name": "Find by internal _id (advanced)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/collections/orders/{{orderId}}",
              "host": ["{{baseUrl}}"],
              "path": ["collections", "orders", "{{orderId}}"]
            },
            "description": "Direct location-map lookup using DocumentForge's internal 16-byte `_id` (the `id` field returned from POST /collections/{name}). Most apps should NOT use this — prefer /by/{field}/{value} above. Useful when you've cached the _id from a prior write and want to skip the index step."
          }
        },
        {
          "name": "Query: point lookup by PNR",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{ \"sql\": \"SELECT * FROM orders WHERE pnr = '{{pnr}}'\" }"
            },
            "url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] },
            "description": "Indexed lookup — should come back in <1ms with plan=INDEX_SCAN."
          }
        },
        {
          "name": "Query: nested path with LIMIT",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{ \"sql\": \"SELECT pnr, passenger.lastName FROM orders WHERE status = 'CONFIRMED' LIMIT 10\" }"
            },
            "url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] }
          }
        },
        {
          "name": "Query: range + aggregate",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{ \"sql\": \"SELECT status, COUNT(*) FROM orders GROUP BY status\" }"
            },
            "url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] }
          }
        },
        {
          "name": "Query: DISTINCT (unique values)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{ \"sql\": \"SELECT DISTINCT passenger.lastName FROM orders LIMIT 20\" }"
            },
            "url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] },
            "description": "DISTINCT modifier strips duplicate projected rows. Combine with WHERE/LIMIT/ORDER BY freely. Plan gets a `+ DISTINCT` suffix. _id is automatically dropped from the projection so duplicates can actually collapse."
          }
        },
        {
          "name": "Query: NDJSON streaming (Accept header)",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/x-ndjson" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{ \"sql\": \"SELECT * FROM orders\" }"
            },
            "url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] },
            "description": "Stream large result sets line-by-line. Line 1 is a meta envelope (count, plan, executionTimeMs), every subsequent line is one full document as raw JSON. Plan / count / time also exposed in X-DFDB-Plan / X-DFDB-Count / X-DFDB-ExecutionMs headers.\n\nClient-side: read line-by-line, parse one JSON per line — no need to materialise the whole array on the heap.\n\nEquivalent without an Accept header: append `?stream=true` to the URL."
          }
        },
        {
          "name": "Update by business key (PUT by PNR)",
          "request": {
            "method": "PUT",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"pnr\": \"{{pnr}}\",\n  \"status\": \"CHANGED\",\n  \"passenger\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Smith\"\n  },\n  \"flights\": [\n    {\n      \"flightNumber\": \"AA100\",\n      \"departureAirport\": \"JFK\",\n      \"arrivalAirport\": \"LHR\",\n      \"fareAmount\": 720\n    }\n  ]\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/collections/orders/by/pnr/{{pnr}}",
              "host": ["{{baseUrl}}"],
              "path": ["collections", "orders", "by", "pnr", "{{pnr}}"]
            },
            "description": "Replace the entire document matching pnr={{pnr}}. Body is the full new JSON. The internal _id of the matched document is re-stamped onto the new doc, so all indexes stay coherent and any cached _id in your app code stays valid.\n\nResponse includes the matchedBy field/value and the execution plan used to find it."
          }
        },
        {
          "name": "Update by internal _id (PUT advanced)",
          "request": {
            "method": "PUT",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"pnr\": \"{{pnr}}\",\n  \"status\": \"CHANGED_VIA_ID\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/collections/orders/{{orderId}}",
              "host": ["{{baseUrl}}"],
              "path": ["collections", "orders", "{{orderId}}"]
            },
            "description": "Replace by internal _id — useful when you've cached the id from a prior write and want to skip the lookup. Most apps should prefer the /by/{field}/{value} variant above."
          }
        },
        {
          "name": "Create an index",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"collection\": \"orders\",\n  \"path\": \"flights[0].departureAirport\",\n  \"name\": \"idx_dep_airport\",\n  \"unique\": false\n}"
            },
            "url": { "raw": "{{baseUrl}}/index", "host": ["{{baseUrl}}"], "path": ["index"] }
          }
        },
        {
          "name": "List indexes on a collection",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/indexes/orders",
              "host": ["{{baseUrl}}"],
              "path": ["indexes", "orders"]
            }
          }
        },
        {
          "name": "Delete by internal _id (advanced)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/collections/orders/{{orderId}}",
              "host": ["{{baseUrl}}"],
              "path": ["collections", "orders", "{{orderId}}"]
            },
            "description": "Direct delete by internal `_id`. Prefer DELETE /by/{field}/{value} unless you've cached the _id."
          }
        }
      ]
    },
    {
      "name": "Database Operations",
      "description": "Endpoints a management app (or SRE) uses to inspect and control the database. Covers admin maintenance, replication, and read-only mode for handover.",
      "item": [
        {
          "name": "Admin",
          "description": "Maintenance operations that don't affect schema or documents.",
          "item": [
            {
              "name": "Flush (force dirty pages to disk)",
              "request": {
                "method": "POST",
                "header": [],
                "url": { "raw": "{{baseUrl}}/admin/flush", "host": ["{{baseUrl}}"], "path": ["admin", "flush"] },
                "description": "Flushes the page cache and truncates the recovery log. Safe to call anytime; takes the write lock briefly."
              }
            },
            {
              "name": "Checkpoint",
              "request": {
                "method": "POST",
                "header": [],
                "url": { "raw": "{{baseUrl}}/admin/checkpoint", "host": ["{{baseUrl}}"], "path": ["admin", "checkpoint"] },
                "description": "Equivalent to flush — drains the WAL and ensures a consistent on-disk state."
              }
            },
            {
              "name": "Compact collection",
              "request": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/admin/compact/orders",
                  "host": ["{{baseUrl}}"],
                  "path": ["admin", "compact", "orders"]
                },
                "description": "Reclaim space from deleted documents. Takes the write lock for the duration. Returns pages compacted + bytes reclaimed."
              }
            },
            {
              "name": "Rebuild indexes",
              "request": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/admin/rebuild-indexes/orders",
                  "host": ["{{baseUrl}}"],
                  "path": ["admin", "rebuild-indexes", "orders"]
                },
                "description": "Rebuild every index on a collection from scratch. Required after bulk insert with ?skipIndexes=true; also useful as a recovery step if you suspect index drift. Scales linearly with collection size × index count — milliseconds for a few thousand docs, seconds for millions."
              }
            },
            {
              "name": "Drop collection (destructive)",
              "request": {
                "method": "DELETE",
                "header": [
                  { "key": "X-Confirm", "value": "true", "description": "Required header to confirm the destructive op." }
                ],
                "url": {
                  "raw": "{{baseUrl}}/collections/tempdata",
                  "host": ["{{baseUrl}}"],
                  "path": ["collections", "tempdata"]
                },
                "description": "Permanently removes a collection and its indexes. Guarded by the `X-Confirm: true` header."
              }
            }
          ]
        },
        {
          "name": "Replication",
          "description": "Control the node's replication role at runtime. You can also set these via CLI flags at startup (`dfdb serve --replication-role leader ...`) — these endpoints are for wiring up topology dynamically or from a management UI.",
          "item": [
            {
              "name": "Status",
              "request": {
                "method": "GET",
                "header": [],
                "url": { "raw": "{{baseUrl}}/replication/status", "host": ["{{baseUrl}}"], "path": ["replication", "status"] },
                "description": "Role, current seq, follower count, last-applied seq on a follower, gaps detected, whether this node was auto-promoted."
              }
            },
            {
              "name": "Start as leader",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{ \"port\": {{replicationPort}} }"
                },
                "url": {
                  "raw": "{{baseUrl}}/replication/start-leader",
                  "host": ["{{baseUrl}}"],
                  "path": ["replication", "start-leader"]
                },
                "description": "Opens the replication listener. Port must differ from the HTTP port. If the node was started with `--replication-secret`, that secret is used automatically."
              }
            },
            {
              "name": "Start as follower",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"host\": \"{{leaderHost}}\",\n  \"port\": {{leaderPort}}\n}"
                },
                "url": {
                  "raw": "{{baseUrl}}/replication/start-follower",
                  "host": ["{{baseUrl}}"],
                  "path": ["replication", "start-follower"]
                },
                "description": "Connects to the leader and begins consuming the op stream."
              }
            },
            {
              "name": "Enter read-only mode (handover step 1)",
              "request": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/replication/read-only",
                  "host": ["{{baseUrl}}"],
                  "path": ["replication", "read-only"]
                },
                "description": "Old leader enters read-only. Writes now fail with a clear error."
              }
            },
            {
              "name": "Promote to leader (handover step 2)",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{ \"port\": {{replicationPort}} }"
                },
                "url": {
                  "raw": "{{baseUrl}}/replication/promote",
                  "host": ["{{baseUrl}}"],
                  "path": ["replication", "promote"]
                },
                "description": "New leader takes over on the given replication port."
              }
            },
            {
              "name": "Exit read-only (recover / abort handover)",
              "request": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/replication/read-write",
                  "host": ["{{baseUrl}}"],
                  "path": ["replication", "read-write"]
                }
              }
            },
            {
              "name": "Enable auto-failover",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"silenceSeconds\": 10,\n  \"newLeaderPort\": {{replicationPort}}\n}"
                },
                "url": {
                  "raw": "{{baseUrl}}/replication/auto-failover/enable",
                  "host": ["{{baseUrl}}"],
                  "path": ["replication", "auto-failover", "enable"]
                },
                "description": "Follower will self-promote if the leader is silent for `silenceSeconds`."
              }
            },
            {
              "name": "Disable auto-failover",
              "request": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/replication/auto-failover/disable",
                  "host": ["{{baseUrl}}"],
                  "path": ["replication", "auto-failover", "disable"]
                }
              }
            }
          ]
        },
        {
          "name": "Sharding (cluster control from any node)",
          "description": "Sharding is coordinated through a cluster config file (`cluster.json`) rather than per-node endpoints. Use the `dfdb` CLI to build and inspect the topology — these requests are here for reference alongside the admin ops.\n\nAnd for the *runtime* side of sharding (data lives on multiple nodes), you route application traffic through any member node's `/query` endpoint; the engine fans out to other shards as needed.",
          "item": [
            {
              "name": "README",
              "request": {
                "method": "GET",
                "header": [],
                "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], "path": ["health"] },
                "description": "Sharding topology is controlled by the `dfdb cluster` CLI subcommand, not HTTP endpoints (it's a file-driven config, not per-node state). Typical flow:\n\n```bash\ndfdb cluster init cluster.json\ndfdb cluster add-shard cluster.json shard-a http://localhost:5001\ndfdb cluster add-shard cluster.json shard-b http://localhost:5002\ndfdb cluster add-shard cluster.json shard-c http://localhost:5003\ndfdb cluster add-collection cluster.json orders   hash pnr\ndfdb cluster add-collection cluster.json airports replicated\ndfdb health    cluster.json\ndfdb rebalance old.json new.json --plan-only\ndfdb rebalance old.json new.json\n```\n\nThis request just hits `/health` on the current node as a no-op placeholder. See the Sharding guide for the full protocol."
              }
            },
            {
              "name": "Ping each shard via /health",
              "request": {
                "method": "GET",
                "header": [],
                "url": { "raw": "http://localhost:5001/health", "host": ["http://localhost:5001"], "path": ["health"] },
                "description": "Duplicate this request per shard — a management UI can run the equivalent of `dfdb health` by polling each shard's /health endpoint."
              }
            }
          ]
        }
      ]
    }
  ]
}
