@aui.io/apollo 0.1.80 → 0.1.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/README.md +3 -3
  2. package/node_modules/@aui.io/apollo-cli/dist/src/commands/bundles.js +21 -5
  3. package/node_modules/@aui.io/apollo-cli/dist/src/commands/bundles.js.map +1 -1
  4. package/node_modules/@aui.io/apollo-cli/dist/src/commands/hierarchy.js +5 -1
  5. package/node_modules/@aui.io/apollo-cli/dist/src/commands/hierarchy.js.map +1 -1
  6. package/node_modules/@aui.io/apollo-cli/dist/src/commands/mockdb.js +182 -10
  7. package/node_modules/@aui.io/apollo-cli/dist/src/commands/mockdb.js.map +1 -1
  8. package/node_modules/@aui.io/apollo-cli/package.json +3 -3
  9. package/node_modules/@aui.io/apollo-core/dist/src/api/builder-api.d.ts +2 -0
  10. package/node_modules/@aui.io/apollo-core/dist/src/api/builder-api.js.map +1 -1
  11. package/node_modules/@aui.io/apollo-core/dist/src/api/mock-db.d.ts +82 -1
  12. package/node_modules/@aui.io/apollo-core/dist/src/api/mock-db.js +66 -3
  13. package/node_modules/@aui.io/apollo-core/dist/src/api/mock-db.js.map +1 -1
  14. package/node_modules/@aui.io/apollo-core/dist/src/services/hierarchy.d.ts +11 -0
  15. package/node_modules/@aui.io/apollo-core/dist/src/services/hierarchy.js +14 -0
  16. package/node_modules/@aui.io/apollo-core/dist/src/services/hierarchy.js.map +1 -1
  17. package/node_modules/@aui.io/apollo-core/dist/src/services/mockdb.d.ts +84 -3
  18. package/node_modules/@aui.io/apollo-core/dist/src/services/mockdb.js +272 -7
  19. package/node_modules/@aui.io/apollo-core/dist/src/services/mockdb.js.map +1 -1
  20. package/node_modules/@aui.io/apollo-core/dist/src/services/runs.d.ts +2 -0
  21. package/node_modules/@aui.io/apollo-core/dist/src/services/runs.js +1 -0
  22. package/node_modules/@aui.io/apollo-core/dist/src/services/runs.js.map +1 -1
  23. package/node_modules/@aui.io/apollo-core/package.json +1 -1
  24. package/node_modules/@aui.io/apollo-tui/package.json +2 -2
  25. package/package.json +4 -4
@@ -1,4 +1,4 @@
1
- import { MockDbClient, type MockDbCloneResponse, type MockDbIdentitiesResponse, type MockDbIdentitiesUpsertResponse, type MockDbProvisionResponse, type MockDbSampleData, type MockDbStatusResponse, type MockDbTokenAuth } from "../api/mock-db.js";
1
+ import { MockDbClient, type MockDbCloneResponse, type MockDbIdentitiesResponse, type MockDbIdentitiesUpsertResponse, type MockDbProvisionResponse, type MockDbResetResponse, type MockDbResetScope, type MockDbRowsPage, type MockDbSampleData, type MockDbSeedMode, type MockDbSeedResponse, type MockDbStatusResponse, type MockDbTokenAuth, type MockDbTruncateResponse } from "../api/mock-db.js";
2
2
  import type { Context } from "../config/index.js";
3
3
  import { keyPrefixes } from "../config/mockdb-keys.js";
4
4
  /** Env var the wired `connections.yaml` reads the runtime key from by default. */
@@ -48,7 +48,88 @@ export declare function statusMockDb(context: Context, options?: MockDbServiceOp
48
48
  export declare function createCollection(context: Context, name: string, schema: unknown, options?: MockDbServiceOptions): Promise<unknown>;
49
49
  export declare function listCollections(context: Context, options?: MockDbServiceOptions): Promise<unknown>;
50
50
  export declare function updateCollection(context: Context, name: string, addColumns: unknown[], options?: MockDbServiceOptions): Promise<unknown>;
51
- export declare function seedRows(context: Context, collection: string, rows: unknown[], options?: MockDbServiceOptions): Promise<unknown>;
51
+ export interface SeedRowsOptions extends MockDbServiceOptions {
52
+ /** append (default) / upsert / replace — see `MockDbSeedOptions`. */
53
+ mode?: MockDbSeedMode;
54
+ /** false → counts only in the reply (the CLI's choice for a file import). */
55
+ echo?: boolean;
56
+ /** replace only: also empty collections that reference this one. */
57
+ cascade?: boolean;
58
+ }
59
+ /** One request. Rows of any count ride it (the service batches its INSERTs); a file import batches on top (`seedRowsFromFile`). */
60
+ export declare function seedRows(context: Context, collection: string, rows: unknown[], options?: SeedRowsOptions): Promise<MockDbSeedResponse>;
61
+ /** Rows per request when a file is imported; well under any body-size limit for ordinary rows. */
62
+ export declare const DEFAULT_SEED_BATCH_SIZE = 500;
63
+ export interface SeedFileOptions extends SeedRowsOptions {
64
+ /** Rows per request (default `DEFAULT_SEED_BATCH_SIZE`). */
65
+ batchSize?: number;
66
+ /** Called after every batch lands. */
67
+ onProgress?: (progress: {
68
+ batch: number;
69
+ batches: number;
70
+ sent: number;
71
+ total: number;
72
+ }) => void;
73
+ }
74
+ export interface SeedFileResult {
75
+ file: string;
76
+ collection: string;
77
+ mode: MockDbSeedMode;
78
+ rowsRead: number;
79
+ batchSize: number;
80
+ batches: number;
81
+ inserted: number;
82
+ updated: number;
83
+ deleted: number;
84
+ /** replace: the collections the first batch emptied, child-first. */
85
+ replaced: string[];
86
+ }
87
+ /**
88
+ * Parse a seed file: a JSON array, the `{ "rows": [...] }` wrapper the service
89
+ * body uses, or NDJSON / JSON Lines (one object per line, blank lines ignored).
90
+ * The shape is detected from the content, not the extension, so `.json`,
91
+ * `.ndjson` and `.jsonl` all work.
92
+ */
93
+ export declare function parseRowsFile(text: string, where: string): unknown[];
94
+ /** Read + parse a seed file from disk. */
95
+ export declare function readRowsFile(file: string): Promise<{
96
+ path: string;
97
+ rows: unknown[];
98
+ }>;
99
+ /**
100
+ * Seed a whole file in batches, sequentially, stopping at the first failure.
101
+ *
102
+ * `replace` is honoured by the FIRST batch only — it truncates and inserts in
103
+ * one service transaction — and the rest append, so the collection ends up
104
+ * holding exactly the file. `upsert` rides every batch (and is safe to
105
+ * re-run); `append` rides every batch. Nothing is auto-retried: a failed batch
106
+ * is reported with exactly which rows landed, so the caller re-sends only
107
+ * what is missing (or re-runs the whole file with `upsert`).
108
+ */
109
+ export declare function seedRowsFromFile(context: Context, collection: string, file: string, options?: SeedFileOptions): Promise<SeedFileResult>;
110
+ export declare function listRows(context: Context, collection: string, page?: {
111
+ limit?: number;
112
+ offset?: number;
113
+ resolve?: boolean;
114
+ }, options?: MockDbServiceOptions): Promise<MockDbRowsPage>;
115
+ export declare function patchRow(context: Context, collection: string, id: string, patch: Record<string, unknown>, options?: MockDbServiceOptions): Promise<unknown>;
116
+ export declare function deleteRow(context: Context, collection: string, id: string, options?: MockDbServiceOptions): Promise<unknown>;
117
+ export declare function truncateCollection(context: Context, name: string, options?: MockDbServiceOptions & {
118
+ cascade?: boolean;
119
+ }): Promise<MockDbTruncateResponse>;
120
+ export declare function deleteCollection(context: Context, name: string, options?: MockDbServiceOptions): Promise<unknown>;
121
+ /**
122
+ * The key-stable reset. The local key cache is deliberately NOT cleared —
123
+ * the service never touched the agent record, so the stored keys are still
124
+ * the live ones; `delete` is the command that forgets them.
125
+ */
126
+ export declare function resetMockDb(context: Context, options?: MockDbServiceOptions & {
127
+ scope?: MockDbResetScope;
128
+ }): Promise<MockDbResetResponse>;
129
+ /** Parse `--mode`; undefined means append. */
130
+ export declare function parseSeedMode(raw: string | undefined): MockDbSeedMode | undefined;
131
+ /** Parse `--scope`; undefined means data. */
132
+ export declare function parseResetScope(raw: string | undefined): MockDbResetScope | undefined;
52
133
  export declare function createEndpoint(context: Context, body: Record<string, unknown>, options?: MockDbServiceOptions): Promise<unknown>;
53
134
  export declare function listEndpoints(context: Context, options?: MockDbServiceOptions): Promise<unknown>;
54
135
  export declare function getEndpoint(context: Context, slug: string, options?: MockDbServiceOptions): Promise<unknown>;
@@ -153,7 +234,7 @@ export declare function normalizeSchema(raw: unknown): {
153
234
  */
154
235
  export declare function normalizeAddColumns(raw: unknown): unknown[];
155
236
  /** The text emitted by `apollo mockdb guide` — the how-to for driving the mock DB. */
156
- export declare const MOCKDB_GUIDE = "# Mock DB \u2014 Apollo CLI Guide\n\nA per-agent **mock database** for building and testing agents that have no\nreal backend (or no test environment) yet. You define collections (tables),\nseed realistic data, and declare named read/write endpoints; the agent's\nprogram sources then call those endpoints at runtime through a\n`connections.yaml` connection. It supports relative date/time fields\n(`now-30d`) that stay fresh, and per-conversation copy-on-write keyed by the\nsession id, so test runs are isolated and resettable.\n\nAll `apollo mockdb` commands are **non-interactive**. Pass the global\n`--json` for the machine envelope (`{ \"success\": true, \"data\": ... }`).\n\n## How auth works (and why it's safe)\n\n- The agent comes from the checkout you are standing in (`.apollorc`) or\n `--agent <id>`; the account from the active project (`--account <id>` or\n `APOLLO_ACCOUNT_ID` to override).\n- Your Apollo token is sent **first-party from the CLI** only where the\n service demands it: `provision`, `status`, `delete`, and minting a\n management key. The service verifies it against Apollo (you must be a\n member of the agent's account).\n- Provision returns two per-agent keys, stored under `~/.apollo/mockdb/`:\n - **managementKey** \u2014 used for ALL other management calls (`X-Mgmt-Key`;\n no Apollo token transmitted). Minted on demand if missing/rejected.\n - **runtimeKey** \u2014 what the agent's connection uses at runtime; also what\n `apollo mockdb execute` uses to test endpoints.\n\n## Typical workflow\n\n```bash\n# 1. Provision (stores the keys) and wire bundle/src/connections.yaml\napollo mockdb provision --wire\n\n# 2. Create collections (parents before children \u2014 FKs need the parent first)\napollo mockdb collections create --name users \\\n --schema '{\"columns\":[{\"name\":\"id\",\"type\":\"text\",\"primaryKey\":true},{\"name\":\"name\",\"type\":\"text\"}]}'\napollo mockdb collections create --name accounts \\\n --schema '{\"columns\":[{\"name\":\"id\",\"type\":\"text\",\"primaryKey\":true},{\"name\":\"user_id\",\"type\":\"text\",\"references\":\"users.id\"},{\"name\":\"balance\",\"type\":\"number\"},{\"name\":\"opened_at\",\"type\":\"reldate\"}]}'\n\n# 3. Seed data (relative date tokens allowed)\napollo mockdb seed --collection users --rows '[{\"id\":\"U1\",\"name\":\"Ada Lovelace\"}]'\napollo mockdb seed --collection accounts --rows '[{\"id\":\"A1\",\"user_id\":\"U1\",\"balance\":4200.5,\"opened_at\":\"now-400d\"}]'\n\n# 4. Declare the endpoints the agent will call\napollo mockdb endpoint create --slug get-accounts \\\n --param-schema '{\"user_id\":{\"type\":\"string\",\"required\":true}}' \\\n --spec '{\"kind\":\"read\",\"collection\":\"accounts\",\"where\":[{\"column\":\"user_id\",\"op\":\"eq\",\"param\":\"user_id\"}]}'\n\n# 4b. A read keyed by a SET of ids: op \"in\" + a param declared { \"type\": \"array\" }\napollo mockdb endpoint create --slug accounts-by-ids \\\n --param-schema '{\"ids\":{\"type\":\"array\",\"required\":true}}' \\\n --spec '{\"kind\":\"read\",\"collection\":\"accounts\",\"where\":[{\"column\":\"id\",\"op\":\"in\",\"param\":\"ids\"}]}'\n\n# 5. Test an endpoint under a session (runtime key + session = a conversation)\napollo mockdb execute --slug get-accounts --session test-1 --body '{\"user_id\":\"U1\"}'\napollo mockdb execute --slug accounts-by-ids --body '{\"ids\":[\"A1\",\"A3\"]}'\n\n# 6. Inspect / reset that session's overlay\napollo mockdb session get --key test-1\napollo mockdb session reset --key test-1\n```\n\nThen map each program source that names `connection: mock-db` to an endpoint\nunder `operations` in `bundle/src/connections.yaml` (`method: POST`,\n`path: /<slug>/execute`, `rows: $.data.rows` -- the stub `wire` writes carries\nthe template), export the runtime key as `MOCKDB_TOKEN` (or the connection's\n`token_env`), and `apollo validate && apollo push`.\n\n## Endpoint params \u2014 `--param-schema` types\n\nEvery param a spec references (`where` clauses, insert/update values) must be\ndeclared: `{ \"<name>\": { \"type\": <type>, \"required\"?: bool } }`. An unknown type\nis a 400 at `endpoint create` / `update`.\n\n| type | accepts |\n|---|---|\n| `string` | any value, stringified |\n| `integer` / `number` | a number (numeric strings accepted) |\n| `boolean` | `true` / `false` (the strings too) |\n| `json` | any JSON value, passed through untouched |\n| `array` | a JSON array -- **what `op: \"in\"` needs**. On a GET query string, `?ids=A1,A2` or `?ids=[\"A1\",\"A2\"]` |\n\nDo not declare a set as `string`: the array is stringified to `\"A1,A2\"` and the\n`in` clause rejects it (`where id op \"in\" needs an array value (param \"ids\"\nresolved to string -- declare it as { \"type\": \"array\" } in paramSchema)`).\n\nOn the runtime side nothing is typed: a list-valued key bound on the source rides a\nPOST body as a JSON array as-is (no `type:` on the connection variable -- the\nloader refuses one), and a query-string variable renders it comma-joined\n(`style: csv`, the default) or repeated (`style: repeat`); mock-db's `array`\nparam accepts either.\n\n## Personas for the playground simulator (`identities`)\n\nOnce the mock DB has data, the playground can offer a **persona picker** before\nthe first message so a tester speaks as a real seeded user. The payload is\nauthored here \u2014 the schema and seed values are already in hand:\n\n```bash\n# Read what's stored (no personas \u21D2 { \"personas\": [] } \u2014 the picker is off).\napollo mockdb identities get\n\n# Upsert the payload (large payloads are easier from a file).\napollo mockdb identities set --file personas.json\napollo mockdb identities set --sample-data '{\n \"instructions\": \"Pick a persona to simulate as.\",\n \"personas\": [\n {\n \"label\": \"Jane Doe (verified)\",\n \"state\": \"verified\",\n \"default\": true,\n \"fields\": [\n { \"key\": \"user_id\", \"label\": \"User ID\", \"value\": \"U1001\", \"group\": \"who_you_are\" },\n { \"key\": \"pin\", \"label\": \"PIN\", \"value\": \"4321\", \"group\": \"verify\", \"role\": \"secret\" }\n ]\n }\n ]\n}'\n```\n\nRules the service enforces (a 400 names the exact `personas[i].fields[j]`):\n\n- Each persona needs a non-empty `label` and `state`; each field needs\n `key`, `label`, `value`, and a `group` of `who_you_are` | `verify` |\n `good_to_know` | `records`.\n- Exactly **one** persona carries `default: true` (when there is at least one).\n- Optional per-field extras: `role` (`key` | `secret`), `audience`\n (`user` | `simulator` | `both`), `required_to_start`, `match`, `maps_to`.\n- Optional per-persona `intents` (`{ title, message }`) seed suggested openers.\n\n`set` is a full **REPLACE**, not a merge \u2014 `{\"personas\":[]}` turns the picker\noff. Seed the values first: every persona value must exist in the mock DB.\nPersonas travel with the DB \u2014 `clone` and `export` carry them.\n\n## Cloning another agent's mock DB\n\n```bash\n# Copy a template agent's collections + rows + endpoints + personas into THIS\n# agent (the target must be empty; your account must own both agents):\napollo mockdb clone --from <templateAgentId>\n```\n\n## Commands\n\n| Command | What it does |\n|---|---|\n| `mockdb provision [--wire]` | Create the agent's mock DB; store runtime + management keys |\n| `mockdb status` | Ownership + local key prefixes + verdict (ok / empty / new / unknown) |\n| `mockdb describe` | Schema, relationships, relative-field flags, endpoints, row counts |\n| `mockdb collections create --name <n> --schema <json>` | Create a collection (table) |\n| `mockdb collections update --name <n> --add-columns <json>` | Add nullable columns (no rename/type-change/drop) |\n| `mockdb collections list` | List collections |\n| `mockdb seed --collection <n> --rows <json-array>` | Seed base rows |\n| `mockdb endpoint create --slug <s> --spec <json> [--param-schema <json>] [--description <t>]` | Declare a read/insert/update/delete endpoint |\n| `mockdb endpoint list` / `get <slug>` | List / inspect endpoints |\n| `mockdb endpoint update <slug> [--spec] [--param-schema] [--description]` | PATCH-merge an endpoint |\n| `mockdb endpoint delete <slug>` | Delete an endpoint (re-map operations + push after) |\n| `mockdb execute --slug <s> [--session <key>] [--body <json>]` | Run an endpoint with the runtime key |\n| `mockdb identities get` / `set --sample-data <json> | --file <path>` | Read / replace the persona payload |\n| `mockdb session get --key <key>` / `reset --key <key>` | Inspect / reset a session overlay |\n| `mockdb rotate mgmt` / `rotate runtime` | Mint fresh keys (re-wire after a runtime rotation) |\n| `mockdb clone --from <id>` | Clone another agent's mock DB into this one (target must be empty) |\n| `mockdb export` | Export schema + base data + endpoints + personas |\n| `mockdb keys` | Show locally stored key prefixes (no secrets) |\n| `mockdb wire [--connection-id] [--token-env]` | Write bundle/src/connections.yaml for the runtime key |\n| `mockdb delete --force` | Permanently delete the mock DB (Apollo token only) |\n| `mockdb guide [--output <file>]` | Write this guide to a file (default GUIDE.md) |\n\nCommon flags: `--agent <id>`, `--account <id>`, and the global `--json`.\n\n## Notes\n\n- Create collections **parent-first** (a `references` FK needs the parent).\n- **Writes need `--session`**; reads may omit it to peek at pristine base.\n- Endpoint `where`/value params must be declared in `--param-schema`, with a\n type the service knows (`string|integer|number|boolean|json|array`); a set fed to\n `op: \"in\"` is `array`.\n- `execute` resolves the runtime key from `$MOCKDB_TOKEN` \u2192 the local cache \u2192\n a fresh mint (then reports `runtimeKeyResync` so you re-wire).\n- A blank 2xx from the service is a transient transport blip: reads retry it,\n writes stop and tell you to verify before retrying (never duplicate rows).\n- Override the service URL with `APOLLO_MOCKDB_URL`.\n";
237
+ export declare const MOCKDB_GUIDE = "# Mock DB \u2014 Apollo CLI Guide\n\nA per-agent **mock database** for building and testing agents that have no\nreal backend (or no test environment) yet. You define collections (tables),\nseed realistic data, and declare named read/write endpoints; the agent's\nprogram sources then call those endpoints at runtime through a\n`connections.yaml` connection. It supports relative date/time fields\n(`now-30d`) that stay fresh, and per-conversation copy-on-write keyed by the\nsession id, so test runs are isolated and resettable.\n\nAll `apollo mockdb` commands are **non-interactive**. Pass the global\n`--json` for the machine envelope (`{ \"success\": true, \"data\": ... }`).\n\n## How auth works (and why it's safe)\n\n- The agent comes from the checkout you are standing in (`.apollorc`) or\n `--agent <id>`; the account from the active project (`--account <id>` or\n `APOLLO_ACCOUNT_ID` to override).\n- Your Apollo token is sent **first-party from the CLI** only where the\n service demands it: `provision`, `status`, `delete`, and minting a\n management key. The service verifies it against Apollo (you must be a\n member of the agent's account).\n- Provision returns two per-agent keys, stored under `~/.apollo/mockdb/`:\n - **managementKey** \u2014 used for ALL other management calls (`X-Mgmt-Key`;\n no Apollo token transmitted). Minted on demand if missing/rejected.\n - **runtimeKey** \u2014 what the agent's connection uses at runtime; also what\n `apollo mockdb execute` uses to test endpoints.\n\n## Typical workflow\n\n```bash\n# 1. Provision (stores the keys) and wire bundle/src/connections.yaml\napollo mockdb provision --wire\n\n# 2. Create collections (parents before children \u2014 FKs need the parent first)\napollo mockdb collections create --name users \\\n --schema '{\"columns\":[{\"name\":\"id\",\"type\":\"text\",\"primaryKey\":true},{\"name\":\"name\",\"type\":\"text\"}]}'\napollo mockdb collections create --name accounts \\\n --schema '{\"columns\":[{\"name\":\"id\",\"type\":\"text\",\"primaryKey\":true},{\"name\":\"user_id\",\"type\":\"text\",\"references\":\"users.id\"},{\"name\":\"balance\",\"type\":\"number\"},{\"name\":\"opened_at\",\"type\":\"reldate\"}]}'\n\n# 3. Seed data (relative date tokens allowed). Inline for a handful of rows,\n# --file (a JSON array or NDJSON) for a real dataset -- any size, batched.\napollo mockdb seed --collection users --rows '[{\"id\":\"U1\",\"name\":\"Ada Lovelace\"}]'\napollo mockdb seed --collection accounts --file accounts.json\n\n# 4. Declare the endpoints the agent will call\napollo mockdb endpoint create --slug get-accounts \\\n --param-schema '{\"user_id\":{\"type\":\"string\",\"required\":true}}' \\\n --spec '{\"kind\":\"read\",\"collection\":\"accounts\",\"where\":[{\"column\":\"user_id\",\"op\":\"eq\",\"param\":\"user_id\"}]}'\n\n# 4b. A read keyed by a SET of ids: op \"in\" + a param declared { \"type\": \"array\" }\napollo mockdb endpoint create --slug accounts-by-ids \\\n --param-schema '{\"ids\":{\"type\":\"array\",\"required\":true}}' \\\n --spec '{\"kind\":\"read\",\"collection\":\"accounts\",\"where\":[{\"column\":\"id\",\"op\":\"in\",\"param\":\"ids\"}]}'\n\n# 5. Test an endpoint under a session (runtime key + session = a conversation)\napollo mockdb execute --slug get-accounts --session test-1 --body '{\"user_id\":\"U1\"}'\napollo mockdb execute --slug accounts-by-ids --body '{\"ids\":[\"A1\",\"A3\"]}'\n\n# 6. Inspect / reset that session's overlay\napollo mockdb session get --key test-1\napollo mockdb session reset --key test-1\n```\n\nThen map each program source that names `connection: mock-db` to an endpoint\nunder `operations` in `bundle/src/connections.yaml` (`method: POST`,\n`path: /<slug>/execute`, `rows: $.data.rows` -- the stub `wire` writes carries\nthe template), export the runtime key as `MOCKDB_TOKEN` (or the connection's\n`token_env`), and `apollo validate && apollo push`.\n\n## Endpoint params \u2014 `--param-schema` types\n\nEvery param a spec references (`where` clauses, insert/update values) must be\ndeclared: `{ \"<name>\": { \"type\": <type>, \"required\"?: bool } }`. An unknown type\nis a 400 at `endpoint create` / `update`.\n\n| type | accepts |\n|---|---|\n| `string` | any value, stringified |\n| `integer` / `number` | a number (numeric strings accepted) |\n| `boolean` | `true` / `false` (the strings too) |\n| `json` | any JSON value, passed through untouched |\n| `array` | a JSON array -- **what `op: \"in\"` needs**. On a GET query string, `?ids=A1,A2` or `?ids=[\"A1\",\"A2\"]` |\n\nDo not declare a set as `string`: the array is stringified to `\"A1,A2\"` and the\n`in` clause rejects it (`where id op \"in\" needs an array value (param \"ids\"\nresolved to string -- declare it as { \"type\": \"array\" } in paramSchema)`).\n\nOn the runtime side nothing is typed: a list-valued key bound on the source rides a\nPOST body as a JSON array as-is (no `type:` on the connection variable -- the\nloader refuses one), and a query-string variable renders it comma-joined\n(`style: csv`, the default) or repeated (`style: repeat`); mock-db's `array`\nparam accepts either.\n\n## Changing data without a drop (keys stay valid)\n\n`mockdb delete --force` forgets the agent, so the next `provision` mints NEW\nkeys and everything wired to the old runtime key -- the vault's `MOCKDB_TOKEN`,\na deployed connection -- breaks until re-wired. None of these touch the keys:\n\n```bash\n# Re-seed everything: empty every collection + session overlay, keep schema,\n# endpoints, personas and keys. Then seed again.\napollo mockdb reset --force\n# Start over from an empty DB with the SAME keys (drops collections + endpoints + personas).\napollo mockdb reset --scope all --force\n\n# One collection: empty it (a parent with populated FK children is refused\n# unless --cascade, which empties the children too), drop it, or edit one row.\napollo mockdb collections truncate --name orders\napollo mockdb collections truncate --name users --cascade\napollo mockdb collections delete --name scratch --force\napollo mockdb rows list --collection users --limit 20\napollo mockdb rows patch --collection users --id U1 --set '{\"tier\":\"gold\"}'\napollo mockdb rows delete --collection users --id U9\n\n# Seed modes: upsert patches rows whose key exists (safe to re-run a file);\n# replace empties the collection first, in one transaction.\napollo mockdb seed --collection users --file users.json --mode upsert\napollo mockdb seed --collection orders --file orders.ndjson --mode replace\n```\n\nA read is never silently capped: `execute` replies `data.total` (the full\nmatch) and `data.hasMore` beside `rows`/`count`. Anything that sums or\ncounts rows must page (`offset` += `count`) until `hasMore` is false -- the\n`pagination:` stanza `wire` writes makes the engine do that on its own.\n\n## Personas for the playground simulator (`identities`)\n\nOnce the mock DB has data, the playground can offer a **persona picker** before\nthe first message so a tester speaks as a real seeded user. The payload is\nauthored here \u2014 the schema and seed values are already in hand:\n\n```bash\n# Read what's stored (no personas \u21D2 { \"personas\": [] } \u2014 the picker is off).\napollo mockdb identities get\n\n# Upsert the payload (large payloads are easier from a file).\napollo mockdb identities set --file personas.json\napollo mockdb identities set --sample-data '{\n \"instructions\": \"Pick a persona to simulate as.\",\n \"personas\": [\n {\n \"label\": \"Jane Doe (verified)\",\n \"state\": \"verified\",\n \"default\": true,\n \"fields\": [\n { \"key\": \"user_id\", \"label\": \"User ID\", \"value\": \"U1001\", \"group\": \"who_you_are\" },\n { \"key\": \"pin\", \"label\": \"PIN\", \"value\": \"4321\", \"group\": \"verify\", \"role\": \"secret\" }\n ]\n }\n ]\n}'\n```\n\nRules the service enforces (a 400 names the exact `personas[i].fields[j]`):\n\n- Each persona needs a non-empty `label` and `state`; each field needs\n `key`, `label`, `value`, and a `group` of `who_you_are` | `verify` |\n `good_to_know` | `records`.\n- Exactly **one** persona carries `default: true` (when there is at least one).\n- Optional per-field extras: `role` (`key` | `secret`), `audience`\n (`user` | `simulator` | `both`), `required_to_start`, `match`, `maps_to`.\n- Optional per-persona `intents` (`{ title, message }`) seed suggested openers.\n\n`set` is a full **REPLACE**, not a merge \u2014 `{\"personas\":[]}` turns the picker\noff. Seed the values first: every persona value must exist in the mock DB.\nPersonas travel with the DB \u2014 `clone` and `export` carry them.\n\n## Cloning another agent's mock DB\n\n```bash\n# Copy a template agent's collections + rows + endpoints + personas into THIS\n# agent (the target must be empty; your account must own both agents):\napollo mockdb clone --from <templateAgentId>\n```\n\n## Commands\n\n| Command | What it does |\n|---|---|\n| `mockdb provision [--wire]` | Create the agent's mock DB; store runtime + management keys |\n| `mockdb status` | Ownership + local key prefixes + verdict (ok / empty / new / unknown) |\n| `mockdb describe` | Schema, relationships, relative-field flags, endpoints, row counts |\n| `mockdb collections create --name <n> --schema <json>` | Create a collection (table) |\n| `mockdb collections update --name <n> --add-columns <json>` | Add nullable columns (no rename/type-change/drop) |\n| `mockdb collections list` | List collections |\n| `mockdb collections truncate --name <n> [--cascade]` | Empty a collection (base rows + its session overlay records); keys stay |\n| `mockdb collections delete --name <n> --force` | Drop a collection and its table |\n| `mockdb seed --collection <n> (--rows <json-array> \\| --file <path>) [--mode append\\|upsert\\|replace] [--batch <n>] [--cascade]` | Seed base rows; a file (JSON array or NDJSON) is sent in batches |\n| `mockdb rows list --collection <n> [--limit] [--offset] [--resolve]` | Page through base rows (with the total) |\n| `mockdb rows patch --collection <n> --id <id> --set <json>` | Edit one base row by primary key |\n| `mockdb rows delete --collection <n> --id <id>` | Remove one base row by primary key |\n| `mockdb reset [--scope data\\|all] --force` | Key-stable reset: `data` empties every collection + sessions; `all` also drops collections, endpoints, personas |\n| `mockdb endpoint create --slug <s> --spec <json> [--param-schema <json>] [--description <t>]` | Declare a read/insert/update/delete endpoint |\n| `mockdb endpoint list` / `get <slug>` | List / inspect endpoints |\n| `mockdb endpoint update <slug> [--spec] [--param-schema] [--description]` | PATCH-merge an endpoint |\n| `mockdb endpoint delete <slug>` | Delete an endpoint (re-map operations + push after) |\n| `mockdb execute --slug <s> [--session <key>] [--body <json>]` | Run an endpoint with the runtime key |\n| `mockdb identities get` / `set --sample-data <json> | --file <path>` | Read / replace the persona payload |\n| `mockdb session get --key <key>` / `reset --key <key>` | Inspect / reset a session overlay |\n| `mockdb rotate mgmt` / `rotate runtime` | Mint fresh keys (re-wire after a runtime rotation) |\n| `mockdb clone --from <id>` | Clone another agent's mock DB into this one (target must be empty) |\n| `mockdb export` | Export schema + base data + endpoints + personas |\n| `mockdb keys` | Show locally stored key prefixes (no secrets) |\n| `mockdb wire [--connection-id] [--token-env]` | Write bundle/src/connections.yaml for the runtime key |\n| `mockdb delete --force` | Permanently delete the mock DB (Apollo token only) -- the next provision mints NEW keys; prefer `reset` |\n| `mockdb guide [--output <file>]` | Write this guide to a file (default GUIDE.md) |\n\nCommon flags: `--agent <id>`, `--account <id>`, and the global `--json`.\n\n## Notes\n\n- Create collections **parent-first** (a `references` FK needs the parent).\n- **Writes need `--session`**; reads may omit it to peek at pristine base.\n- Endpoint `where`/value params must be declared in `--param-schema`, with a\n type the service knows (`string|integer|number|boolean|json|array`); a set fed to\n `op: \"in\"` is `array`.\n- `execute` resolves the runtime key from `$MOCKDB_TOKEN` \u2192 the local cache \u2192\n a fresh mint (then reports `runtimeKeyResync` so you re-wire).\n- A read replies at most `limit` rows (default 100, cap 1000) and says so:\n `data.total` / `data.hasMore`. Page with `offset` until `hasMore` is false.\n- A blank 2xx from the service is a transient transport blip: reads retry it,\n writes stop and tell you to verify before retrying (never duplicate rows).\n- Override the service URL with `APOLLO_MOCKDB_URL`.\n";
157
238
  /** Write the guide to `outputPath` (default `GUIDE.md` in the cwd). */
158
239
  export declare function writeMockDbGuide(outputPath?: string): Promise<{
159
240
  written: true;
@@ -28,7 +28,7 @@
28
28
  import { mkdir, readFile, writeFile } from "node:fs/promises";
29
29
  import { homedir } from "node:os";
30
30
  import path from "node:path";
31
- import { isMockDbTransportError, MockDbClient, mockDbErrorCode, } from "../api/mock-db.js";
31
+ import { isMockDbTransportError, MOCKDB_RESET_SCOPES, MOCKDB_SEED_MODES, MockDbClient, mockDbErrorCode, } from "../api/mock-db.js";
32
32
  import { clearMockDbKeys, keyPrefixes, loadMockDbKeys, saveMockDbKeys, } from "../config/mockdb-keys.js";
33
33
  import { ApiError, AuthError, CliError, ConfigError, ValidationError, } from "../errors/index.js";
34
34
  /** Env var the wired `connections.yaml` reads the runtime key from by default. */
@@ -158,6 +158,10 @@ function blankBodyError(resolved, op, kind) {
158
158
  tail =
159
159
  "No key was stored, so nothing changed and it is safe to re-run. If it persists, the backend key-mint path is failing — check `apollo mockdb status` and report it.";
160
160
  }
161
+ else if (kind === "idempotent") {
162
+ tail =
163
+ "The operation may or may not have applied, and re-running it is SAFE (it is idempotent) — check with `apollo mockdb describe`, then re-run.";
164
+ }
161
165
  else {
162
166
  tail =
163
167
  "The write MAY have already landed server-side, so do NOT blindly retry (re-running an insert/seed can DUPLICATE rows). Verify first — `apollo mockdb describe` / `collections list` for base writes, `apollo mockdb session get --key <session>` for runtime writes — then retry only what's actually missing.";
@@ -171,6 +175,17 @@ async function readWithRetry(resolved, op, fn) {
171
175
  blankBodyError(resolved, op, "read");
172
176
  return result;
173
177
  }
178
+ /**
179
+ * Run an IDEMPOTENT change (a reset, a truncate, a row patch/delete): a blank
180
+ * body is retried like a read — repeating it cannot duplicate anything — and
181
+ * a persistent blank fails with the honest word for it.
182
+ */
183
+ async function idempotentWithRetry(resolved, op, fn) {
184
+ const result = await retryWhile(isEmptyReadObject, fn);
185
+ if (isEmptyReadObject(result))
186
+ blankBodyError(resolved, op, "idempotent");
187
+ return result;
188
+ }
174
189
  function isAuthRejection(error) {
175
190
  return error instanceof ApiError && (error.status === 401 || error.status === 403);
176
191
  }
@@ -361,14 +376,210 @@ export async function updateCollection(context, name, addColumns, options = {})
361
376
  blankBodyError(resolved, "collections update", "write");
362
377
  return resp;
363
378
  }
379
+ /** One request. Rows of any count ride it (the service batches its INSERTs); a file import batches on top (`seedRowsFromFile`). */
364
380
  export async function seedRows(context, collection, rows, options = {}) {
365
381
  const resolved = resolveMockDb(context, options);
366
- // Seeding APPENDS rows — never auto-retry a blank (a repeat could duplicate).
367
- const resp = await withManage(resolved, (auth) => resolved.client.seedRows(resolved.agentId, auth, collection, rows));
382
+ const seedOptions = {
383
+ ...(options.mode === undefined ? {} : { mode: options.mode }),
384
+ ...(options.echo === undefined ? {} : { echo: options.echo }),
385
+ ...(options.cascade === undefined ? {} : { cascade: options.cascade }),
386
+ };
387
+ // A seed WRITES rows — never auto-retry a blank (an append repeat could duplicate).
388
+ const resp = await withManage(resolved, (auth) => resolved.client.seedRows(resolved.agentId, auth, collection, rows, seedOptions));
368
389
  if (isEmptyReadObject(resp))
369
390
  blankBodyError(resolved, "seed", "write");
370
391
  return resp;
371
392
  }
393
+ /** Rows per request when a file is imported; well under any body-size limit for ordinary rows. */
394
+ export const DEFAULT_SEED_BATCH_SIZE = 500;
395
+ /**
396
+ * Parse a seed file: a JSON array, the `{ "rows": [...] }` wrapper the service
397
+ * body uses, or NDJSON / JSON Lines (one object per line, blank lines ignored).
398
+ * The shape is detected from the content, not the extension, so `.json`,
399
+ * `.ndjson` and `.jsonl` all work.
400
+ */
401
+ export function parseRowsFile(text, where) {
402
+ const trimmed = text.trim();
403
+ if (!trimmed)
404
+ throw new ValidationError(`${where} is empty.`, "Give it a JSON array of rows, or one JSON object per line.");
405
+ if (trimmed.startsWith("[")) {
406
+ const parsed = parseJsonFlag(where, trimmed);
407
+ if (!Array.isArray(parsed))
408
+ throw new ValidationError(`${where} must be a JSON array of rows.`);
409
+ return parsed;
410
+ }
411
+ if (trimmed.startsWith("{")) {
412
+ // One whole-file object carrying `rows` is the wrapper, not a one-row NDJSON file.
413
+ let whole;
414
+ try {
415
+ whole = JSON.parse(trimmed);
416
+ }
417
+ catch {
418
+ whole = undefined; // more than one line, or not JSON at all: NDJSON below decides
419
+ }
420
+ const wrapped = whole?.rows;
421
+ if (Array.isArray(wrapped))
422
+ return wrapped;
423
+ }
424
+ const rows = [];
425
+ const lines = trimmed.split(/\r?\n/);
426
+ lines.forEach((line, i) => {
427
+ if (!line.trim())
428
+ return;
429
+ let parsed;
430
+ try {
431
+ parsed = JSON.parse(line);
432
+ }
433
+ catch (error) {
434
+ throw new ValidationError(`${where} line ${i + 1} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, "A seed file is either one JSON array, or one JSON object per line (NDJSON).");
435
+ }
436
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
437
+ throw new ValidationError(`${where} line ${i + 1} must be a JSON object (a row).`);
438
+ }
439
+ rows.push(parsed);
440
+ });
441
+ return rows;
442
+ }
443
+ /** Read + parse a seed file from disk. */
444
+ export async function readRowsFile(file) {
445
+ const resolvedPath = path.resolve(file);
446
+ let raw;
447
+ try {
448
+ raw = await readFile(resolvedPath, "utf8");
449
+ }
450
+ catch (error) {
451
+ throw new ValidationError(`--file could not be read: ${error instanceof Error ? error.message : String(error)}`, `Check that ${resolvedPath} exists and is readable.`);
452
+ }
453
+ return { path: resolvedPath, rows: parseRowsFile(raw, `--file (${resolvedPath})`) };
454
+ }
455
+ /**
456
+ * Seed a whole file in batches, sequentially, stopping at the first failure.
457
+ *
458
+ * `replace` is honoured by the FIRST batch only — it truncates and inserts in
459
+ * one service transaction — and the rest append, so the collection ends up
460
+ * holding exactly the file. `upsert` rides every batch (and is safe to
461
+ * re-run); `append` rides every batch. Nothing is auto-retried: a failed batch
462
+ * is reported with exactly which rows landed, so the caller re-sends only
463
+ * what is missing (or re-runs the whole file with `upsert`).
464
+ */
465
+ export async function seedRowsFromFile(context, collection, file, options = {}) {
466
+ const mode = options.mode ?? "append";
467
+ const batchSize = options.batchSize ?? DEFAULT_SEED_BATCH_SIZE;
468
+ if (!Number.isInteger(batchSize) || batchSize < 1) {
469
+ throw new ValidationError("--batch must be a positive integer.", `The default is ${String(DEFAULT_SEED_BATCH_SIZE)} rows per request.`);
470
+ }
471
+ const { path: resolvedPath, rows } = await readRowsFile(file);
472
+ if (rows.length === 0)
473
+ throw new ValidationError(`${resolvedPath} holds no rows.`);
474
+ const batches = Math.ceil(rows.length / batchSize);
475
+ const result = {
476
+ file: resolvedPath,
477
+ collection,
478
+ mode,
479
+ rowsRead: rows.length,
480
+ batchSize,
481
+ batches,
482
+ inserted: 0,
483
+ updated: 0,
484
+ deleted: 0,
485
+ replaced: [],
486
+ };
487
+ let sent = 0;
488
+ for (let batch = 0; batch < batches; batch++) {
489
+ const slice = rows.slice(batch * batchSize, (batch + 1) * batchSize);
490
+ // Only the first batch may truncate; the ones after it must land beside it.
491
+ const batchMode = mode === "replace" && batch > 0 ? "append" : mode;
492
+ let resp;
493
+ try {
494
+ resp = await seedRows(context, collection, slice, {
495
+ ...(options.agentId === undefined ? {} : { agentId: options.agentId }),
496
+ ...(options.accountId === undefined ? {} : { accountId: options.accountId }),
497
+ ...(options.tokenEnv === undefined ? {} : { tokenEnv: options.tokenEnv }),
498
+ ...(options.homeDir === undefined ? {} : { homeDir: options.homeDir }),
499
+ ...(options.fetch === undefined ? {} : { fetch: options.fetch }),
500
+ mode: batchMode,
501
+ echo: false,
502
+ // cascade belongs to the truncating batch only.
503
+ ...(batchMode === "replace" ? { cascade: options.cascade === true } : {}),
504
+ });
505
+ }
506
+ catch (error) {
507
+ const first = batch * batchSize;
508
+ const last = first + slice.length - 1;
509
+ let landed;
510
+ if (batch === 0) {
511
+ landed =
512
+ mode === "replace"
513
+ ? "Nothing changed: a replace is one transaction, so the collection still holds its previous rows."
514
+ : "Nothing from the file is in the collection.";
515
+ }
516
+ else {
517
+ landed = `Rows 0-${String(first - 1)} of the file (${String(sent)} rows, ${String(batch)} batch${batch === 1 ? "" : "es"}) are in the collection${mode === "replace" ? " (the first batch replaced the old rows)" : ""}; nothing from this batch or after it is.`;
518
+ }
519
+ throw new CliError(`mockdb seed --file: batch ${String(batch + 1)}/${String(batches)} (rows ${String(first)}-${String(last)} of ${resolvedPath}) failed: ${error instanceof Error ? error.message : String(error)}`, {
520
+ code: error instanceof CliError ? error.code : "API_SERVER_ERROR",
521
+ suggestion: `${landed} NOT auto-retried. Fix the rows and re-send from row ${String(first)} ` +
522
+ `(or re-run the whole file with --mode upsert, which is safe to repeat).`,
523
+ cause: error,
524
+ });
525
+ }
526
+ sent += slice.length;
527
+ result.inserted += resp.inserted ?? 0;
528
+ result.updated += resp.updated ?? 0;
529
+ result.deleted += resp.deleted ?? 0;
530
+ if (batch === 0 && Array.isArray(resp.replaced))
531
+ result.replaced = resp.replaced.map(String);
532
+ options.onProgress?.({ batch: batch + 1, batches, sent, total: rows.length });
533
+ }
534
+ return result;
535
+ }
536
+ export async function listRows(context, collection, page = {}, options = {}) {
537
+ const resolved = resolveMockDb(context, options);
538
+ return readWithRetry(resolved, "rows list", () => withManage(resolved, (auth) => resolved.client.listRows(resolved.agentId, auth, collection, page)));
539
+ }
540
+ export async function patchRow(context, collection, id, patch, options = {}) {
541
+ const resolved = resolveMockDb(context, options);
542
+ return idempotentWithRetry(resolved, "rows patch", () => withManage(resolved, (auth) => resolved.client.patchRow(resolved.agentId, auth, collection, id, patch)));
543
+ }
544
+ export async function deleteRow(context, collection, id, options = {}) {
545
+ const resolved = resolveMockDb(context, options);
546
+ return idempotentWithRetry(resolved, "rows delete", () => withManage(resolved, (auth) => resolved.client.deleteRow(resolved.agentId, auth, collection, id)));
547
+ }
548
+ export async function truncateCollection(context, name, options = {}) {
549
+ const resolved = resolveMockDb(context, options);
550
+ return idempotentWithRetry(resolved, "collections truncate", () => withManage(resolved, (auth) => resolved.client.truncateCollection(resolved.agentId, auth, name, { cascade: options.cascade === true })));
551
+ }
552
+ export async function deleteCollection(context, name, options = {}) {
553
+ const resolved = resolveMockDb(context, options);
554
+ return idempotentWithRetry(resolved, "collections delete", () => withManage(resolved, (auth) => resolved.client.deleteCollection(resolved.agentId, auth, name)));
555
+ }
556
+ /**
557
+ * The key-stable reset. The local key cache is deliberately NOT cleared —
558
+ * the service never touched the agent record, so the stored keys are still
559
+ * the live ones; `delete` is the command that forgets them.
560
+ */
561
+ export async function resetMockDb(context, options = {}) {
562
+ const resolved = resolveMockDb(context, options);
563
+ return idempotentWithRetry(resolved, "reset", () => withManage(resolved, (auth) => resolved.client.resetAgent(resolved.agentId, auth, options.scope === undefined ? {} : { scope: options.scope })));
564
+ }
565
+ /** Parse `--mode`; undefined means append. */
566
+ export function parseSeedMode(raw) {
567
+ if (raw === undefined)
568
+ return undefined;
569
+ const value = raw.trim();
570
+ if (MOCKDB_SEED_MODES.includes(value))
571
+ return value;
572
+ throw new ValidationError(`--mode must be one of ${MOCKDB_SEED_MODES.join(", ")} (got ${JSON.stringify(raw)}).`, "append inserts (duplicate key → 409); upsert patches rows whose key exists; replace empties the collection first.");
573
+ }
574
+ /** Parse `--scope`; undefined means data. */
575
+ export function parseResetScope(raw) {
576
+ if (raw === undefined)
577
+ return undefined;
578
+ const value = raw.trim();
579
+ if (MOCKDB_RESET_SCOPES.includes(value))
580
+ return value;
581
+ throw new ValidationError(`--scope must be one of ${MOCKDB_RESET_SCOPES.join(", ")} (got ${JSON.stringify(raw)}).`, "data empties every collection and clears sessions (schema + endpoints + personas stay); all also drops collections, endpoints and personas. Keys survive both.");
582
+ }
372
583
  export async function createEndpoint(context, body, options = {}) {
373
584
  const resolved = resolveMockDb(context, options);
374
585
  const resp = await withManage(resolved, (auth) => resolved.client.createEndpoint(resolved.agentId, auth, body));
@@ -776,6 +987,18 @@ export async function wireMockDbConnection(context, options = {}) {
776
987
  ` # method: POST`,
777
988
  ` # path: /<endpoint-slug>/execute`,
778
989
  ` # rows: $.data.rows`,
990
+ ` # # A read replies 100 rows per page (data.total / data.hasMore say`,
991
+ ` # # when it was capped). Keep pagination on every read a program may`,
992
+ ` # # sum or count: the engine then walks the pages by offset instead`,
993
+ ` # # of totalling the first hundred, and \`more:\` lets it stop on the`,
994
+ ` # # server's own "no more" (needs a runtime that carries \`more:\` --`,
995
+ ` # # every release up to 0.10.25 refuses the key: drop that one line`,
996
+ ` # # there, never the pagination).`,
997
+ ` # pagination:`,
998
+ ` # style: offset`,
999
+ ` # request: offset`,
1000
+ ` # max_pages: 10`,
1001
+ ` # more: $.data.hasMore`,
779
1002
  ``,
780
1003
  ].join("\n");
781
1004
  let existing = "";
@@ -894,9 +1117,10 @@ apollo mockdb collections create --name users \\
894
1117
  apollo mockdb collections create --name accounts \\
895
1118
  --schema '{"columns":[{"name":"id","type":"text","primaryKey":true},{"name":"user_id","type":"text","references":"users.id"},{"name":"balance","type":"number"},{"name":"opened_at","type":"reldate"}]}'
896
1119
 
897
- # 3. Seed data (relative date tokens allowed)
1120
+ # 3. Seed data (relative date tokens allowed). Inline for a handful of rows,
1121
+ # --file (a JSON array or NDJSON) for a real dataset -- any size, batched.
898
1122
  apollo mockdb seed --collection users --rows '[{"id":"U1","name":"Ada Lovelace"}]'
899
- apollo mockdb seed --collection accounts --rows '[{"id":"A1","user_id":"U1","balance":4200.5,"opened_at":"now-400d"}]'
1123
+ apollo mockdb seed --collection accounts --file accounts.json
900
1124
 
901
1125
  # 4. Declare the endpoints the agent will call
902
1126
  apollo mockdb endpoint create --slug get-accounts \\
@@ -947,6 +1171,39 @@ loader refuses one), and a query-string variable renders it comma-joined
947
1171
  (\`style: csv\`, the default) or repeated (\`style: repeat\`); mock-db's \`array\`
948
1172
  param accepts either.
949
1173
 
1174
+ ## Changing data without a drop (keys stay valid)
1175
+
1176
+ \`mockdb delete --force\` forgets the agent, so the next \`provision\` mints NEW
1177
+ keys and everything wired to the old runtime key -- the vault's \`MOCKDB_TOKEN\`,
1178
+ a deployed connection -- breaks until re-wired. None of these touch the keys:
1179
+
1180
+ \`\`\`bash
1181
+ # Re-seed everything: empty every collection + session overlay, keep schema,
1182
+ # endpoints, personas and keys. Then seed again.
1183
+ apollo mockdb reset --force
1184
+ # Start over from an empty DB with the SAME keys (drops collections + endpoints + personas).
1185
+ apollo mockdb reset --scope all --force
1186
+
1187
+ # One collection: empty it (a parent with populated FK children is refused
1188
+ # unless --cascade, which empties the children too), drop it, or edit one row.
1189
+ apollo mockdb collections truncate --name orders
1190
+ apollo mockdb collections truncate --name users --cascade
1191
+ apollo mockdb collections delete --name scratch --force
1192
+ apollo mockdb rows list --collection users --limit 20
1193
+ apollo mockdb rows patch --collection users --id U1 --set '{"tier":"gold"}'
1194
+ apollo mockdb rows delete --collection users --id U9
1195
+
1196
+ # Seed modes: upsert patches rows whose key exists (safe to re-run a file);
1197
+ # replace empties the collection first, in one transaction.
1198
+ apollo mockdb seed --collection users --file users.json --mode upsert
1199
+ apollo mockdb seed --collection orders --file orders.ndjson --mode replace
1200
+ \`\`\`
1201
+
1202
+ A read is never silently capped: \`execute\` replies \`data.total\` (the full
1203
+ match) and \`data.hasMore\` beside \`rows\`/\`count\`. Anything that sums or
1204
+ counts rows must page (\`offset\` += \`count\`) until \`hasMore\` is false -- the
1205
+ \`pagination:\` stanza \`wire\` writes makes the engine do that on its own.
1206
+
950
1207
  ## Personas for the playground simulator (\`identities\`)
951
1208
 
952
1209
  Once the mock DB has data, the playground can offer a **persona picker** before
@@ -1007,7 +1264,13 @@ apollo mockdb clone --from <templateAgentId>
1007
1264
  | \`mockdb collections create --name <n> --schema <json>\` | Create a collection (table) |
1008
1265
  | \`mockdb collections update --name <n> --add-columns <json>\` | Add nullable columns (no rename/type-change/drop) |
1009
1266
  | \`mockdb collections list\` | List collections |
1010
- | \`mockdb seed --collection <n> --rows <json-array>\` | Seed base rows |
1267
+ | \`mockdb collections truncate --name <n> [--cascade]\` | Empty a collection (base rows + its session overlay records); keys stay |
1268
+ | \`mockdb collections delete --name <n> --force\` | Drop a collection and its table |
1269
+ | \`mockdb seed --collection <n> (--rows <json-array> \\| --file <path>) [--mode append\\|upsert\\|replace] [--batch <n>] [--cascade]\` | Seed base rows; a file (JSON array or NDJSON) is sent in batches |
1270
+ | \`mockdb rows list --collection <n> [--limit] [--offset] [--resolve]\` | Page through base rows (with the total) |
1271
+ | \`mockdb rows patch --collection <n> --id <id> --set <json>\` | Edit one base row by primary key |
1272
+ | \`mockdb rows delete --collection <n> --id <id>\` | Remove one base row by primary key |
1273
+ | \`mockdb reset [--scope data\\|all] --force\` | Key-stable reset: \`data\` empties every collection + sessions; \`all\` also drops collections, endpoints, personas |
1011
1274
  | \`mockdb endpoint create --slug <s> --spec <json> [--param-schema <json>] [--description <t>]\` | Declare a read/insert/update/delete endpoint |
1012
1275
  | \`mockdb endpoint list\` / \`get <slug>\` | List / inspect endpoints |
1013
1276
  | \`mockdb endpoint update <slug> [--spec] [--param-schema] [--description]\` | PATCH-merge an endpoint |
@@ -1020,7 +1283,7 @@ apollo mockdb clone --from <templateAgentId>
1020
1283
  | \`mockdb export\` | Export schema + base data + endpoints + personas |
1021
1284
  | \`mockdb keys\` | Show locally stored key prefixes (no secrets) |
1022
1285
  | \`mockdb wire [--connection-id] [--token-env]\` | Write bundle/src/connections.yaml for the runtime key |
1023
- | \`mockdb delete --force\` | Permanently delete the mock DB (Apollo token only) |
1286
+ | \`mockdb delete --force\` | Permanently delete the mock DB (Apollo token only) -- the next provision mints NEW keys; prefer \`reset\` |
1024
1287
  | \`mockdb guide [--output <file>]\` | Write this guide to a file (default GUIDE.md) |
1025
1288
 
1026
1289
  Common flags: \`--agent <id>\`, \`--account <id>\`, and the global \`--json\`.
@@ -1034,6 +1297,8 @@ Common flags: \`--agent <id>\`, \`--account <id>\`, and the global \`--json\`.
1034
1297
  \`op: "in"\` is \`array\`.
1035
1298
  - \`execute\` resolves the runtime key from \`$MOCKDB_TOKEN\` → the local cache →
1036
1299
  a fresh mint (then reports \`runtimeKeyResync\` so you re-wire).
1300
+ - A read replies at most \`limit\` rows (default 100, cap 1000) and says so:
1301
+ \`data.total\` / \`data.hasMore\`. Page with \`offset\` until \`hasMore\` is false.
1037
1302
  - A blank 2xx from the service is a transient transport blip: reads retry it,
1038
1303
  writes stop and tell you to verify before retrying (never duplicate rows).
1039
1304
  - Override the service URL with \`APOLLO_MOCKDB_URL\`.