@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.
- package/README.md +3 -3
- package/node_modules/@aui.io/apollo-cli/dist/src/commands/bundles.js +21 -5
- package/node_modules/@aui.io/apollo-cli/dist/src/commands/bundles.js.map +1 -1
- package/node_modules/@aui.io/apollo-cli/dist/src/commands/hierarchy.js +5 -1
- package/node_modules/@aui.io/apollo-cli/dist/src/commands/hierarchy.js.map +1 -1
- package/node_modules/@aui.io/apollo-cli/dist/src/commands/mockdb.js +182 -10
- package/node_modules/@aui.io/apollo-cli/dist/src/commands/mockdb.js.map +1 -1
- package/node_modules/@aui.io/apollo-cli/package.json +3 -3
- package/node_modules/@aui.io/apollo-core/dist/src/api/builder-api.d.ts +2 -0
- package/node_modules/@aui.io/apollo-core/dist/src/api/builder-api.js.map +1 -1
- package/node_modules/@aui.io/apollo-core/dist/src/api/mock-db.d.ts +82 -1
- package/node_modules/@aui.io/apollo-core/dist/src/api/mock-db.js +66 -3
- package/node_modules/@aui.io/apollo-core/dist/src/api/mock-db.js.map +1 -1
- package/node_modules/@aui.io/apollo-core/dist/src/services/hierarchy.d.ts +11 -0
- package/node_modules/@aui.io/apollo-core/dist/src/services/hierarchy.js +14 -0
- package/node_modules/@aui.io/apollo-core/dist/src/services/hierarchy.js.map +1 -1
- package/node_modules/@aui.io/apollo-core/dist/src/services/mockdb.d.ts +84 -3
- package/node_modules/@aui.io/apollo-core/dist/src/services/mockdb.js +272 -7
- package/node_modules/@aui.io/apollo-core/dist/src/services/mockdb.js.map +1 -1
- package/node_modules/@aui.io/apollo-core/dist/src/services/runs.d.ts +2 -0
- package/node_modules/@aui.io/apollo-core/dist/src/services/runs.js +1 -0
- package/node_modules/@aui.io/apollo-core/dist/src/services/runs.js.map +1 -1
- package/node_modules/@aui.io/apollo-core/package.json +1 -1
- package/node_modules/@aui.io/apollo-tui/package.json +2 -2
- 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
|
|
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
|
-
|
|
367
|
-
|
|
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 --
|
|
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
|
|
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\`.
|