@prisma-idb/family-idb 0.9.0 → 0.10.0
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 +1 -1
- package/dist/bin/prisma-idb.mjs +1 -1
- package/dist/{cli-CNdK-sPS.mjs → cli-Dhspvkvy.mjs} +4 -4
- package/dist/{cli-CNdK-sPS.mjs.map → cli-Dhspvkvy.mjs.map} +1 -1
- package/dist/exports/cli.mjs +1 -1
- package/dist/exports/control.mjs +4 -260
- package/dist/exports/control.mjs.map +1 -1
- package/dist/{preflight-4jDCNvS0.mjs → preflight-Bx2q040D.mjs} +79 -7
- package/dist/preflight-Bx2q040D.mjs.map +1 -0
- package/dist/schema-verify-B7-xiHem.mjs +262 -0
- package/dist/schema-verify-B7-xiHem.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/preflight-4jDCNvS0.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ IndexedDB only exists in the browser, so three migration jobs need their own com
|
|
|
14
14
|
| ------------------------------------- | ------------------------------------------------------------------------------- |
|
|
15
15
|
| `prisma-idb migration plan` | Plans the next migration from the newest one on disk. Warns if it deletes data. |
|
|
16
16
|
| `prisma-idb migration contract-space` | Bundles the migrations into a module the browser imports. |
|
|
17
|
-
| `prisma-idb migration preflight` |
|
|
17
|
+
| `prisma-idb migration preflight` | Replays migrations in memory and verifies the schema against the head snapshot. |
|
|
18
18
|
|
|
19
19
|
All three read `prisma.config.ts`. See [Migrations](https://prisma-idb.dev/docs/prisma-8/migrations).
|
|
20
20
|
|
package/dist/bin/prisma-idb.mjs
CHANGED
|
@@ -220,7 +220,7 @@ const commands = {
|
|
|
220
220
|
"migration preflight": defineIdbCommand({
|
|
221
221
|
help: {
|
|
222
222
|
summary: "Validate the migration chain against fake-indexeddb",
|
|
223
|
-
description: "Walks every migration package under <migrationsDir>/app/ in chain order,\napplying each package's ops.json against a fresh fake-indexeddb instance.\
|
|
223
|
+
description: "Walks every migration package under <migrationsDir>/app/ in chain order,\napplying each package's ops.json against a fresh fake-indexeddb instance.\nVerifies the final stores and indexes against the head contract snapshot.\nFails on replay errors, schema drift, or a missing or mismatched head snapshot.",
|
|
224
224
|
examples: ["migration preflight"]
|
|
225
225
|
},
|
|
226
226
|
args: { flags: { migrationsDir: flag.string({
|
|
@@ -229,7 +229,7 @@ const commands = {
|
|
|
229
229
|
}) } },
|
|
230
230
|
handler: async (args, ctx) => {
|
|
231
231
|
const migrationsDir = resolveMigrationsDir(ctx.config, ctx.cwd, args.flags.migrationsDir);
|
|
232
|
-
const { runPreflight } = await import("./preflight-
|
|
232
|
+
const { runPreflight } = await import("./preflight-Bx2q040D.mjs");
|
|
233
233
|
const sink = createCollectingSink();
|
|
234
234
|
const exitCode = await runPreflight({
|
|
235
235
|
migrationsDir,
|
|
@@ -255,7 +255,7 @@ const idbCommandFamily = defineCommandFamily({
|
|
|
255
255
|
});
|
|
256
256
|
//#endregion
|
|
257
257
|
//#region package.json
|
|
258
|
-
var version = "0.
|
|
258
|
+
var version = "0.10.0";
|
|
259
259
|
//#endregion
|
|
260
260
|
//#region src/cli/cli.ts
|
|
261
261
|
const BIN_NAME = "prisma-idb";
|
|
@@ -329,4 +329,4 @@ async function runIdbCli(proc) {
|
|
|
329
329
|
//#endregion
|
|
330
330
|
export { idbCommandFamily as a, runIdbCli as i, BIN_NAME as n, createIdbCli as r, BIN_GROUPS as t };
|
|
331
331
|
|
|
332
|
-
//# sourceMappingURL=cli-
|
|
332
|
+
//# sourceMappingURL=cli-Dhspvkvy.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-CNdK-sPS.mjs","names":["packageJson.version"],"sources":["../src/cli/collecting-sink.ts","../src/cli/define-command.ts","../src/cli/migration/paths.ts","../src/cli/migration/contract-space.ts","../src/cli/migration/plan.ts","../src/cli/migration/preflight.ts","../src/cli/family.ts","../package.json","../src/cli/cli.ts"],"sourcesContent":["import type { Presentations } from \"@prisma/cli-engine\";\n\n/**\n * Collects a wrapped core function's `out`/`err` sink writes so a command\n * handler can present them through the engine instead of writing to the\n * real `process.stdout`/`process.stderr` directly (which would corrupt\n * `--json` mode and be invisible to `createTestCli`'s in-memory harness).\n */\nexport interface CollectingSink {\n readonly out: (line: string) => void;\n readonly err: (line: string) => void;\n /** Every line written to either sink, in order, newline-split. */\n lines(): readonly string[];\n /**\n * `lines()` joined back into text — the full collected log (progress\n * AND error detail, not just the `err`-only parts), for use as a\n * `CliStructuredError`'s `summary` on a nonzero exit. A wrapped function\n * writes its error explanation to `err` only after already writing\n * per-step progress to `out` (e.g. preflight's `\"<dir> … \"` / `\"FAILED\"`\n * pair) — using only the `err` writes would silently drop that context.\n */\n fullText(): string;\n}\n\n/**\n * Buffers writes the way a real stream does: a write with no trailing\n * newline (e.g. the wrapped functions' `out(\" ${dirName} … \")` followed\n * later by a separate `out(\"ok\\n\")` call) stays pending and joins with\n * whatever arrives next, rather than becoming its own line. Splitting each\n * `out`/`err` call independently — the first cut of this — silently broke\n * exactly this pattern in `preflight.ts`'s per-package progress line.\n */\nexport function createCollectingSink(): CollectingSink {\n const completedLines: string[] = [];\n let buffer = \"\";\n\n function push(text: string): void {\n buffer += text;\n const parts = buffer.split(\"\\n\");\n buffer = parts.pop() ?? \"\";\n for (const part of parts) {\n if (part.length > 0) completedLines.push(part);\n }\n }\n\n const lines = (): readonly string[] => (buffer.length > 0 ? [...completedLines, buffer] : completedLines);\n\n return {\n out: (text) => push(text),\n err: (text) => push(text),\n lines,\n fullText: () => lines().join(\"\\n\").trim(),\n };\n}\n\n/**\n * Renders collected lines as a `drawing` block (verbatim, no reflow) in\n * human mode; `stdout` stays empty since these commands carry no separate\n * machine-pipeable data channel beyond the human log itself.\n */\nexport function presentationsFromSink(sink: CollectingSink, exitCode: number): Presentations {\n const lines = sink.lines();\n return {\n human: () => (lines.length === 0 ? [] : [{ kind: \"drawing\", lines }]),\n stdout: () => [],\n json: () => ({ exitCode, lines }),\n next: () => [],\n };\n}\n","import type { PrismaNextConfig } from \"@prisma/orm-toolchain/config-loader\";\nimport type {\n ArgsSpec,\n CommandDefinition,\n FlagSpec,\n Handler,\n HelpSpec,\n PositionalSpec,\n SpawnDeclarations,\n} from \"@prisma/cli-engine\";\nimport { defineCommand } from \"@prisma/cli-engine\";\nimport { CliStructuredError, notOk } from \"@prisma/cli-engine/protocol\";\nimport { ormConfigSection } from \"./config-section\";\n\nfunction normalizeError(error: unknown): CliStructuredError {\n if (CliStructuredError.is(error)) return error;\n return new CliStructuredError(\"IDB-CLI.UNEXPECTED_ERROR\", error instanceof Error ? error.message : String(error));\n}\n\n/**\n * These 3 commands are IDB-specific (fake-indexeddb, IdbMigrationPlanner) —\n * pointing them at a non-IDB `prisma.config.ts` would fail confusingly deep\n * inside that logic rather than with a clear, actionable error. `orm`'s own\n * config section is family-agnostic by design (ADR 150), so this guard is\n * this shell's responsibility, not the section's.\n */\nfunction requireIdbFamily(config: PrismaNextConfig): void {\n const familyId = config.family?.familyId;\n if (familyId !== \"idb\") {\n throw new CliStructuredError(\n \"IDB-CLI.FAMILY_MISMATCH\",\n `config family is \"${familyId ?? \"unknown\"}\", expected \"idb\".`,\n {\n why: \"This CLI's migration commands operate on IDB-family projects only.\",\n nextActions: [\n {\n kind: \"edit-file\",\n label:\n \"Point --config at your browser-side prisma.config.ts (the one using @prisma-idb/family-idb/control)\",\n },\n ],\n }\n );\n }\n}\n\n/**\n * `defineCommand` pre-bound to the `orm` config section (see\n * `config-section.ts`) with a uniform error boundary, mirroring\n * `@prisma/orm-toolchain`'s own `defineOrmCommand`. The section's schema\n * resolves `contract.output`/`migrations.dir` against the config file that\n * wrote them (and defaults `migrations.dir`), so handlers always read\n * absolute paths without a separate finalize step.\n */\nexport function defineIdbCommand<\n TFlags extends Record<string, FlagSpec<unknown>> = Record<never, FlagSpec<unknown>>,\n TPositionals extends Record<string, PositionalSpec<unknown>> = Record<never, PositionalSpec<unknown>>,\n TCode extends number = never,\n>(\n def: {\n readonly help: HelpSpec;\n readonly args?: ArgsSpec<TFlags, TPositionals>;\n readonly exitCodes?: Readonly<Record<TCode, string>>;\n readonly handler: Handler<TFlags, TPositionals, PrismaNextConfig, TCode, false, false>;\n } & SpawnDeclarations\n): CommandDefinition<TFlags, TPositionals, PrismaNextConfig, TCode, false, false> {\n return defineCommand<TFlags, TPositionals, PrismaNextConfig, TCode, false, false>({\n ...def,\n needs: { config: ormConfigSection },\n handler: async (args, ctx) => {\n try {\n requireIdbFamily(ctx.config);\n return await def.handler(args, ctx);\n } catch (error) {\n return notOk(normalizeError(error));\n }\n },\n });\n}\n","import type { PrismaNextConfig } from \"@prisma/orm-toolchain/config-loader\";\nimport { CliStructuredError } from \"@prisma/cli-engine/protocol\";\nimport { resolve } from \"pathe\";\n\n/**\n * Resolves the contract/migrations paths a command needs from the already\n * finalized (absolute-paths) `orm` config section, with CLI flag overrides\n * winning when supplied. `ctx.config` arrives finalized by\n * `defineIdbCommand` (see `../define-command.ts`), so `config.contract.output`\n * and `config.migrations.dir` are already absolute — only an explicit\n * `--contract`/`--migrations-dir` flag (relative to `cwd`) needs resolving\n * here.\n */\nexport function requireContractPath(config: PrismaNextConfig, cwd: string, override: string | undefined): string {\n if (override !== undefined) return resolve(cwd, override);\n const output = config.contract?.output;\n if (output === undefined) {\n throw new CliStructuredError(\n \"IDB-CLI.CONTRACT_PATH_MISSING\",\n \"config.contract.output is required to resolve the contract path.\",\n {\n why: \"Neither --contract nor prisma.config.ts's orm.contract.output supplied a path.\",\n nextActions: [\n { kind: \"edit-file\", label: \"Set contract.output in prisma.config.ts, or pass --contract explicitly\" },\n ],\n }\n );\n }\n return output;\n}\n\n/**\n * `finalizeConfig` always resolves `migrations.dir` to an absolute path\n * (defaulting to `<configDir>/migrations` when unset); the fallback here is\n * defensive, not the expected path.\n */\nexport function resolveMigrationsDir(config: PrismaNextConfig, cwd: string, override: string | undefined): string {\n if (override !== undefined) return resolve(cwd, override);\n return config.migrations?.dir ?? resolve(cwd, \"migrations\");\n}\n","import { dirname, join, resolve } from \"pathe\";\nimport { flag } from \"@prisma/cli-engine\";\nimport { CliStructuredError, notOk, ok } from \"@prisma/cli-engine/protocol\";\nimport { createCollectingSink, presentationsFromSink } from \"../collecting-sink\";\nimport { defineIdbCommand } from \"../define-command\";\nimport { requireContractPath, resolveMigrationsDir } from \"./paths\";\n\nexport const migrationContractSpaceCommand = defineIdbCommand({\n help: {\n summary: \"Regenerate contract-space.generated.ts from migrations/app/\",\n description:\n \"Reads every migration package under <migrationsDir>/app/, validates the\\n\" +\n \"chain's connectivity, and emits a generated TypeScript module that bundles\\n\" +\n \"them into a ContractSpace.\",\n examples: [\"migration contract-space\", \"migration contract-space --out src/lib/prisma/contract-space.ts\"],\n },\n args: {\n flags: {\n contract: flag.string({\n brief: \"Path to contract.json (default: orm.contract.output)\",\n placeholder: \"path\",\n }),\n migrationsDir: flag.string({\n brief: \"Path to the migrations root (default: orm.migrations.dir)\",\n placeholder: \"path\",\n }),\n out: flag.string({\n brief: \"Output file path (default: colocated with the resolved contract.json)\",\n placeholder: \"path\",\n }),\n },\n },\n handler: async (args, ctx) => {\n const contractPath = requireContractPath(ctx.config, ctx.cwd, args.flags.contract);\n const migrationsDir = resolveMigrationsDir(ctx.config, ctx.cwd, args.flags.migrationsDir);\n const outPath =\n args.flags.out !== undefined\n ? resolve(ctx.cwd, args.flags.out)\n : join(dirname(contractPath), \"contract-space.generated.ts\");\n\n const { generateContractSpace } = await import(\"../../core/contract-space-codegen\");\n const sink = createCollectingSink();\n const exitCode = await generateContractSpace({\n migrationsDir,\n contractPath,\n outPath,\n out: sink.out,\n err: sink.err,\n });\n\n if (exitCode !== 0) {\n return notOk(\n new CliStructuredError(\"IDB-CLI.CONTRACT_SPACE_FAILED\", sink.fullText() || \"migration contract-space failed.\")\n );\n }\n return ok(ctx.present({ data: { exitCode, outPath } }, presentationsFromSink(sink, exitCode)));\n },\n});\n","import { flag } from \"@prisma/cli-engine\";\nimport { CliStructuredError, notOk, ok } from \"@prisma/cli-engine/protocol\";\nimport { createCollectingSink, presentationsFromSink } from \"../collecting-sink\";\nimport { defineIdbCommand } from \"../define-command\";\nimport { requireContractPath, resolveMigrationsDir } from \"./paths\";\n\nexport const migrationPlanCommand = defineIdbCommand({\n help: {\n summary: \"Plan the next IDB migration package (auto-detects baseline vs. incremental)\",\n description:\n \"Compares the emitted contract against the latest on-disk migration state and\\n\" +\n \"produces a new migration package with the required operations. Offline —\\n\" +\n \"never opens IndexedDB (there is no live database on the Node side to consult).\",\n examples: [\"migration plan\", \"migration plan --name add-posts\", \"migration plan --space idb-sync\"],\n },\n args: {\n flags: {\n name: flag.string({\n brief: 'Directory slug (default: \"baseline\" for a fresh space; required otherwise)',\n placeholder: \"slug\",\n }),\n space: flag.string({\n brief: 'Contract-space id (default: \"app\"). Only extension authors set this.',\n placeholder: \"id\",\n }),\n contract: flag.string({\n brief: \"Path to contract.json (default: orm.contract.output)\",\n placeholder: \"path\",\n }),\n migrationsDir: flag.string({\n brief: \"Path to the migrations root (default: orm.migrations.dir)\",\n placeholder: \"path\",\n }),\n },\n },\n handler: async (args, ctx) => {\n const contractPath = requireContractPath(ctx.config, ctx.cwd, args.flags.contract);\n const migrationsDir = resolveMigrationsDir(ctx.config, ctx.cwd, args.flags.migrationsDir);\n\n const { migrationPlan } = await import(\"../../core/migration-plan\");\n const sink = createCollectingSink();\n const exitCode = await migrationPlan({\n migrationsDir,\n contractPath,\n out: sink.out,\n err: sink.err,\n ...(args.flags.name !== undefined && { name: args.flags.name }),\n ...(args.flags.space !== undefined && { spaceId: args.flags.space }),\n });\n\n if (exitCode === 2) {\n return notOk(\n new CliStructuredError(\n \"IDB-CLI.MIGRATION_NAME_REQUIRED\",\n sink.fullText() || \"--name <slug> is required when generating an incremental migration.\"\n )\n );\n }\n if (exitCode !== 0) {\n return notOk(\n new CliStructuredError(\"IDB-CLI.MIGRATION_PLAN_FAILED\", sink.fullText() || \"migration plan failed.\")\n );\n }\n return ok(ctx.present({ data: { exitCode } }, presentationsFromSink(sink, exitCode)));\n },\n});\n","import { flag } from \"@prisma/cli-engine\";\nimport { CliStructuredError, notOk, ok } from \"@prisma/cli-engine/protocol\";\nimport { createCollectingSink, presentationsFromSink } from \"../collecting-sink\";\nimport { defineIdbCommand } from \"../define-command\";\nimport { resolveMigrationsDir } from \"./paths\";\n\nexport const migrationPreflightCommand = defineIdbCommand({\n help: {\n summary: \"Validate the migration chain against fake-indexeddb\",\n description:\n \"Walks every migration package under <migrationsDir>/app/ in chain order,\\n\" +\n \"applying each package's ops.json against a fresh fake-indexeddb instance.\\n\" +\n \"Catches 'the chain doesn't apply cleanly' — not schema drift.\",\n examples: [\"migration preflight\"],\n },\n args: {\n flags: {\n migrationsDir: flag.string({\n brief: \"Path to the migrations root (default: orm.migrations.dir)\",\n placeholder: \"path\",\n }),\n },\n },\n handler: async (args, ctx) => {\n const migrationsDir = resolveMigrationsDir(ctx.config, ctx.cwd, args.flags.migrationsDir);\n\n const { runPreflight } = await import(\"../../core/preflight\");\n const sink = createCollectingSink();\n const exitCode = await runPreflight({ migrationsDir, out: sink.out, err: sink.err });\n\n if (exitCode !== 0) {\n return notOk(new CliStructuredError(\"IDB-CLI.PREFLIGHT_FAILED\", sink.fullText() || \"Preflight failed.\"));\n }\n return ok(ctx.present({ data: { exitCode } }, presentationsFromSink(sink, exitCode)));\n },\n});\n","import type { AnyCommand } from \"@prisma/cli-engine\";\nimport { defineCommandFamily } from \"@prisma/cli-engine\";\nimport { ormConfigSection } from \"./config-section\";\nimport { migrationContractSpaceCommand } from \"./migration/contract-space\";\nimport { migrationPlanCommand } from \"./migration/plan\";\nimport { migrationPreflightCommand } from \"./migration/preflight\";\n\nconst commands: Readonly<Record<string, AnyCommand>> = {\n \"migration plan\": migrationPlanCommand,\n \"migration contract-space\": migrationContractSpaceCommand,\n \"migration preflight\": migrationPreflightCommand,\n};\n\n/**\n * The unit this package contributes to a `@prisma/cli-engine` shell: its\n * config section (reused from the ORM family, see `config-section.ts`) and\n * its 3 commands. These have no generic equivalent — the real `prisma` CLI's\n * `migration new`/`plan`/etc. work against a live database or a SQL/Mongo\n * on-disk model; ours works against `fake-indexeddb` and a hand-rolled\n * contract-space convention.\n */\nexport const idbCommandFamily = defineCommandFamily({\n configSection: ormConfigSection,\n commands,\n});\n","","import type { Cli, HostProcess, Runtime } from \"@prisma/cli-engine\";\nimport { createCli, loadConfig } from \"@prisma/cli-engine\";\nimport { CliStructuredError } from \"@prisma/cli-engine/protocol\";\nimport packageJson from \"../../package.json\" with { type: \"json\" };\nimport { idbCommandFamily } from \"./family\";\n\nexport const BIN_NAME = \"prisma-idb\";\n\nexport const BIN_GROUPS = {\n migration: {\n brief: \"On-disk migration management commands\",\n description:\n \"Plan, bundle, and validate IDB migration packages. Migrations are\\n\" +\n \"contract-to-contract edges stored as versioned directories under migrations/.\",\n },\n} as const;\n\nexport function createIdbCli(): Cli {\n return createCli({\n name: BIN_NAME,\n version: packageJson.version,\n commandFamilies: [idbCommandFamily],\n groups: BIN_GROUPS,\n commands: idbCommandFamily.commands,\n help: {\n tagline: \"IDB-target tooling for Prisma 8\",\n description: \"Migration authoring for IndexedDB projects — no live database on the Node side to consult.\",\n },\n });\n}\n\n/** Everything environmental the engine is given, adapted from the host process once. */\nexport function runtimeFromProcess(proc: HostProcess): Runtime {\n return {\n stdout: { write: (text) => void proc.stdout.write(text) },\n stderr: { write: (text) => void proc.stderr.write(text) },\n stdin: proc.stdin,\n cwd: proc.cwd(),\n env: proc.env,\n isTty: {\n stdin: proc.stdin.isTTY === true,\n stdout: proc.stdout.isTTY === true,\n stderr: proc.stderr.isTTY === true,\n },\n host: {\n runtime: { name: \"node\", version: proc.version },\n platform: proc.platform,\n arch: proc.arch,\n },\n exit: (code) => proc.exit(code),\n onSignal: (callback) => {\n const onInterrupt = () => callback(\"SIGINT\");\n const onTerminate = () => callback(\"SIGTERM\");\n proc.on(\"SIGINT\", onInterrupt);\n proc.on(\"SIGTERM\", onTerminate);\n return () => {\n proc.off(\"SIGINT\", onInterrupt);\n proc.off(\"SIGTERM\", onTerminate);\n };\n },\n loadConfig: (configPath) => loadConfig(proc.cwd(), configPath),\n // Never reached: no command here declares `credentials`/`managesCredentials`,\n // so ctx.api is never constructed — required on Runtime regardless.\n managementApi: { baseUrl: \"https://api.prisma.io\" },\n };\n}\n\nconst STARTUP_FAILURE_EXIT_CODE = 1;\n\nfunction reportStartupFailure(proc: HostProcess, error: unknown): number {\n const normalized = CliStructuredError.is(error)\n ? error\n : new CliStructuredError(\"IDB-CLI.STARTUP_FAILURE\", error instanceof Error ? error.message : String(error));\n proc.stderr.write(`✘ [${normalized.code}] ${normalized.message}\\n`);\n return STARTUP_FAILURE_EXIT_CODE;\n}\n\n/** Parses, executes and settles one invocation; returns the exit code. */\nexport async function runIdbCli(proc: HostProcess): Promise<number> {\n try {\n return await createIdbCli().run(proc.argv.slice(2), runtimeFromProcess(proc));\n } catch (error) {\n return reportStartupFailure(proc, error);\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgCA,SAAgB,uBAAuC;CACrD,MAAM,iBAA2B,CAAC;CAClC,IAAI,SAAS;CAEb,SAAS,KAAK,MAAoB;EAChC,UAAU;EACV,MAAM,QAAQ,OAAO,MAAM,IAAI;EAC/B,SAAS,MAAM,IAAI,KAAK;EACxB,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,SAAS,GAAG,eAAe,KAAK,IAAI;CAEjD;CAEA,MAAM,cAAkC,OAAO,SAAS,IAAI,CAAC,GAAG,gBAAgB,MAAM,IAAI;CAE1F,OAAO;EACL,MAAM,SAAS,KAAK,IAAI;EACxB,MAAM,SAAS,KAAK,IAAI;EACxB;EACA,gBAAgB,MAAM,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK;CAC1C;AACF;;;;;;AAOA,SAAgB,sBAAsB,MAAsB,UAAiC;CAC3F,MAAM,QAAQ,KAAK,MAAM;CACzB,OAAO;EACL,aAAc,MAAM,WAAW,IAAI,CAAC,IAAI,CAAC;GAAE,MAAM;GAAW;EAAM,CAAC;EACnE,cAAc,CAAC;EACf,aAAa;GAAE;GAAU;EAAM;EAC/B,YAAY,CAAC;CACf;AACF;;;ACtDA,SAAS,eAAe,OAAoC;CAC1D,IAAI,mBAAmB,GAAG,KAAK,GAAG,OAAO;CACzC,OAAO,IAAI,mBAAmB,4BAA4B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAClH;;;;;;;;AASA,SAAS,iBAAiB,QAAgC;CACxD,MAAM,WAAW,OAAO,QAAQ;CAChC,IAAI,aAAa,OACf,MAAM,IAAI,mBACR,2BACA,qBAAqB,YAAY,UAAU,qBAC3C;EACE,KAAK;EACL,aAAa,CACX;GACE,MAAM;GACN,OACE;EACJ,CACF;CACF,CACF;AAEJ;;;;;;;;;AAUA,SAAgB,iBAKd,KAMgF;CAChF,OAAO,cAA2E;EAChF,GAAG;EACH,OAAO,EAAE,QAAQ,iBAAiB;EAClC,SAAS,OAAO,MAAM,QAAQ;GAC5B,IAAI;IACF,iBAAiB,IAAI,MAAM;IAC3B,OAAO,MAAM,IAAI,QAAQ,MAAM,GAAG;GACpC,SAAS,OAAO;IACd,OAAO,MAAM,eAAe,KAAK,CAAC;GACpC;EACF;CACF,CAAC;AACH;;;;;;;;;;;;ACjEA,SAAgB,oBAAoB,QAA0B,KAAa,UAAsC;CAC/G,IAAI,aAAa,KAAA,GAAW,OAAO,QAAQ,KAAK,QAAQ;CACxD,MAAM,SAAS,OAAO,UAAU;CAChC,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,mBACR,iCACA,oEACA;EACE,KAAK;EACL,aAAa,CACX;GAAE,MAAM;GAAa,OAAO;EAAyE,CACvG;CACF,CACF;CAEF,OAAO;AACT;;;;;;AAOA,SAAgB,qBAAqB,QAA0B,KAAa,UAAsC;CAChH,IAAI,aAAa,KAAA,GAAW,OAAO,QAAQ,KAAK,QAAQ;CACxD,OAAO,OAAO,YAAY,OAAO,QAAQ,KAAK,YAAY;AAC5D;;;AChCA,MAAa,gCAAgC,iBAAiB;CAC5D,MAAM;EACJ,SAAS;EACT,aACE;EAGF,UAAU,CAAC,4BAA4B,iEAAiE;CAC1G;CACA,MAAM,EACJ,OAAO;EACL,UAAU,KAAK,OAAO;GACpB,OAAO;GACP,aAAa;EACf,CAAC;EACD,eAAe,KAAK,OAAO;GACzB,OAAO;GACP,aAAa;EACf,CAAC;EACD,KAAK,KAAK,OAAO;GACf,OAAO;GACP,aAAa;EACf,CAAC;CACH,EACF;CACA,SAAS,OAAO,MAAM,QAAQ;EAC5B,MAAM,eAAe,oBAAoB,IAAI,QAAQ,IAAI,KAAK,KAAK,MAAM,QAAQ;EACjF,MAAM,gBAAgB,qBAAqB,IAAI,QAAQ,IAAI,KAAK,KAAK,MAAM,aAAa;EACxF,MAAM,UACJ,KAAK,MAAM,QAAQ,KAAA,IACf,QAAQ,IAAI,KAAK,KAAK,MAAM,GAAG,IAC/B,KAAK,QAAQ,YAAY,GAAG,6BAA6B;EAE/D,MAAM,EAAE,0BAA0B,MAAM,OAAO;EAC/C,MAAM,OAAO,qBAAqB;EAClC,MAAM,WAAW,MAAM,sBAAsB;GAC3C;GACA;GACA;GACA,KAAK,KAAK;GACV,KAAK,KAAK;EACZ,CAAC;EAED,IAAI,aAAa,GACf,OAAO,MACL,IAAI,mBAAmB,iCAAiC,KAAK,SAAS,KAAK,kCAAkC,CAC/G;EAEF,OAAO,GAAG,IAAI,QAAQ,EAAE,MAAM;GAAE;GAAU;EAAQ,EAAE,GAAG,sBAAsB,MAAM,QAAQ,CAAC,CAAC;CAC/F;AACF,CAAC;;;AGlDD,MAAM,WAAiD;CACrD,kBFFkC,iBAAiB;EACnD,MAAM;GACJ,SAAS;GACT,aACE;GAGF,UAAU;IAAC;IAAkB;IAAmC;GAAiC;EACnG;EACA,MAAM,EACJ,OAAO;GACL,MAAM,KAAK,OAAO;IAChB,OAAO;IACP,aAAa;GACf,CAAC;GACD,OAAO,KAAK,OAAO;IACjB,OAAO;IACP,aAAa;GACf,CAAC;GACD,UAAU,KAAK,OAAO;IACpB,OAAO;IACP,aAAa;GACf,CAAC;GACD,eAAe,KAAK,OAAO;IACzB,OAAO;IACP,aAAa;GACf,CAAC;EACH,EACF;EACA,SAAS,OAAO,MAAM,QAAQ;GAC5B,MAAM,eAAe,oBAAoB,IAAI,QAAQ,IAAI,KAAK,KAAK,MAAM,QAAQ;GACjF,MAAM,gBAAgB,qBAAqB,IAAI,QAAQ,IAAI,KAAK,KAAK,MAAM,aAAa;GAExF,MAAM,EAAE,kBAAkB,MAAM,OAAO;GACvC,MAAM,OAAO,qBAAqB;GAClC,MAAM,WAAW,MAAM,cAAc;IACnC;IACA;IACA,KAAK,KAAK;IACV,KAAK,KAAK;IACV,GAAI,KAAK,MAAM,SAAS,KAAA,KAAa,EAAE,MAAM,KAAK,MAAM,KAAK;IAC7D,GAAI,KAAK,MAAM,UAAU,KAAA,KAAa,EAAE,SAAS,KAAK,MAAM,MAAM;GACpE,CAAC;GAED,IAAI,aAAa,GACf,OAAO,MACL,IAAI,mBACF,mCACA,KAAK,SAAS,KAAK,qEACrB,CACF;GAEF,IAAI,aAAa,GACf,OAAO,MACL,IAAI,mBAAmB,iCAAiC,KAAK,SAAS,KAAK,wBAAwB,CACrG;GAEF,OAAO,GAAG,IAAI,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,sBAAsB,MAAM,QAAQ,CAAC,CAAC;EACtF;CACF,CEzDoB;CAClB,4BAA4B;CAC5B,uBDJuC,iBAAiB;EACxD,MAAM;GACJ,SAAS;GACT,aACE;GAGF,UAAU,CAAC,qBAAqB;EAClC;EACA,MAAM,EACJ,OAAO,EACL,eAAe,KAAK,OAAO;GACzB,OAAO;GACP,aAAa;EACf,CAAC,EACH,EACF;EACA,SAAS,OAAO,MAAM,QAAQ;GAC5B,MAAM,gBAAgB,qBAAqB,IAAI,QAAQ,IAAI,KAAK,KAAK,MAAM,aAAa;GAExF,MAAM,EAAE,iBAAiB,MAAM,OAAO;GACtC,MAAM,OAAO,qBAAqB;GAClC,MAAM,WAAW,MAAM,aAAa;IAAE;IAAe,KAAK,KAAK;IAAK,KAAK,KAAK;GAAI,CAAC;GAEnF,IAAI,aAAa,GACf,OAAO,MAAM,IAAI,mBAAmB,4BAA4B,KAAK,SAAS,KAAK,mBAAmB,CAAC;GAEzG,OAAO,GAAG,IAAI,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,sBAAsB,MAAM,QAAQ,CAAC,CAAC;EACtF;CACF,CCzByB;AACzB;;;;;;;;;AAUA,MAAa,mBAAmB,oBAAoB;CAClD,eAAe;CACf;AACF,CAAC;;;;;;AElBD,MAAa,WAAW;AAExB,MAAa,aAAa,EACxB,WAAW;CACT,OAAO;CACP,aACE;AAEJ,EACF;AAEA,SAAgB,eAAoB;CAClC,OAAO,UAAU;EACf,MAAM;EACGA;EACT,iBAAiB,CAAC,gBAAgB;EAClC,QAAQ;EACR,UAAU,iBAAiB;EAC3B,MAAM;GACJ,SAAS;GACT,aAAa;EACf;CACF,CAAC;AACH;;AAGA,SAAgB,mBAAmB,MAA4B;CAC7D,OAAO;EACL,QAAQ,EAAE,QAAQ,SAAS,KAAK,KAAK,OAAO,MAAM,IAAI,EAAE;EACxD,QAAQ,EAAE,QAAQ,SAAS,KAAK,KAAK,OAAO,MAAM,IAAI,EAAE;EACxD,OAAO,KAAK;EACZ,KAAK,KAAK,IAAI;EACd,KAAK,KAAK;EACV,OAAO;GACL,OAAO,KAAK,MAAM,UAAU;GAC5B,QAAQ,KAAK,OAAO,UAAU;GAC9B,QAAQ,KAAK,OAAO,UAAU;EAChC;EACA,MAAM;GACJ,SAAS;IAAE,MAAM;IAAQ,SAAS,KAAK;GAAQ;GAC/C,UAAU,KAAK;GACf,MAAM,KAAK;EACb;EACA,OAAO,SAAS,KAAK,KAAK,IAAI;EAC9B,WAAW,aAAa;GACtB,MAAM,oBAAoB,SAAS,QAAQ;GAC3C,MAAM,oBAAoB,SAAS,SAAS;GAC5C,KAAK,GAAG,UAAU,WAAW;GAC7B,KAAK,GAAG,WAAW,WAAW;GAC9B,aAAa;IACX,KAAK,IAAI,UAAU,WAAW;IAC9B,KAAK,IAAI,WAAW,WAAW;GACjC;EACF;EACA,aAAa,eAAe,WAAW,KAAK,IAAI,GAAG,UAAU;EAG7D,eAAe,EAAE,SAAS,wBAAwB;CACpD;AACF;AAEA,MAAM,4BAA4B;AAElC,SAAS,qBAAqB,MAAmB,OAAwB;CACvE,MAAM,aAAa,mBAAmB,GAAG,KAAK,IAC1C,QACA,IAAI,mBAAmB,2BAA2B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;CAC5G,KAAK,OAAO,MAAM,MAAM,WAAW,KAAK,IAAI,WAAW,QAAQ,GAAG;CAClE,OAAO;AACT;;AAGA,eAAsB,UAAU,MAAoC;CAClE,IAAI;EACF,OAAO,MAAM,aAAa,CAAC,CAAC,IAAI,KAAK,KAAK,MAAM,CAAC,GAAG,mBAAmB,IAAI,CAAC;CAC9E,SAAS,OAAO;EACd,OAAO,qBAAqB,MAAM,KAAK;CACzC;AACF"}
|
|
1
|
+
{"version":3,"file":"cli-Dhspvkvy.mjs","names":["packageJson.version"],"sources":["../src/cli/collecting-sink.ts","../src/cli/define-command.ts","../src/cli/migration/paths.ts","../src/cli/migration/contract-space.ts","../src/cli/migration/plan.ts","../src/cli/migration/preflight.ts","../src/cli/family.ts","../package.json","../src/cli/cli.ts"],"sourcesContent":["import type { Presentations } from \"@prisma/cli-engine\";\n\n/**\n * Collects a wrapped core function's `out`/`err` sink writes so a command\n * handler can present them through the engine instead of writing to the\n * real `process.stdout`/`process.stderr` directly (which would corrupt\n * `--json` mode and be invisible to `createTestCli`'s in-memory harness).\n */\nexport interface CollectingSink {\n readonly out: (line: string) => void;\n readonly err: (line: string) => void;\n /** Every line written to either sink, in order, newline-split. */\n lines(): readonly string[];\n /**\n * `lines()` joined back into text — the full collected log (progress\n * AND error detail, not just the `err`-only parts), for use as a\n * `CliStructuredError`'s `summary` on a nonzero exit. A wrapped function\n * writes its error explanation to `err` only after already writing\n * per-step progress to `out` (e.g. preflight's `\"<dir> … \"` / `\"FAILED\"`\n * pair) — using only the `err` writes would silently drop that context.\n */\n fullText(): string;\n}\n\n/**\n * Buffers writes the way a real stream does: a write with no trailing\n * newline (e.g. the wrapped functions' `out(\" ${dirName} … \")` followed\n * later by a separate `out(\"ok\\n\")` call) stays pending and joins with\n * whatever arrives next, rather than becoming its own line. Splitting each\n * `out`/`err` call independently — the first cut of this — silently broke\n * exactly this pattern in `preflight.ts`'s per-package progress line.\n */\nexport function createCollectingSink(): CollectingSink {\n const completedLines: string[] = [];\n let buffer = \"\";\n\n function push(text: string): void {\n buffer += text;\n const parts = buffer.split(\"\\n\");\n buffer = parts.pop() ?? \"\";\n for (const part of parts) {\n if (part.length > 0) completedLines.push(part);\n }\n }\n\n const lines = (): readonly string[] => (buffer.length > 0 ? [...completedLines, buffer] : completedLines);\n\n return {\n out: (text) => push(text),\n err: (text) => push(text),\n lines,\n fullText: () => lines().join(\"\\n\").trim(),\n };\n}\n\n/**\n * Renders collected lines as a `drawing` block (verbatim, no reflow) in\n * human mode; `stdout` stays empty since these commands carry no separate\n * machine-pipeable data channel beyond the human log itself.\n */\nexport function presentationsFromSink(sink: CollectingSink, exitCode: number): Presentations {\n const lines = sink.lines();\n return {\n human: () => (lines.length === 0 ? [] : [{ kind: \"drawing\", lines }]),\n stdout: () => [],\n json: () => ({ exitCode, lines }),\n next: () => [],\n };\n}\n","import type { PrismaNextConfig } from \"@prisma/orm-toolchain/config-loader\";\nimport type {\n ArgsSpec,\n CommandDefinition,\n FlagSpec,\n Handler,\n HelpSpec,\n PositionalSpec,\n SpawnDeclarations,\n} from \"@prisma/cli-engine\";\nimport { defineCommand } from \"@prisma/cli-engine\";\nimport { CliStructuredError, notOk } from \"@prisma/cli-engine/protocol\";\nimport { ormConfigSection } from \"./config-section\";\n\nfunction normalizeError(error: unknown): CliStructuredError {\n if (CliStructuredError.is(error)) return error;\n return new CliStructuredError(\"IDB-CLI.UNEXPECTED_ERROR\", error instanceof Error ? error.message : String(error));\n}\n\n/**\n * These 3 commands are IDB-specific (fake-indexeddb, IdbMigrationPlanner) —\n * pointing them at a non-IDB `prisma.config.ts` would fail confusingly deep\n * inside that logic rather than with a clear, actionable error. `orm`'s own\n * config section is family-agnostic by design (ADR 150), so this guard is\n * this shell's responsibility, not the section's.\n */\nfunction requireIdbFamily(config: PrismaNextConfig): void {\n const familyId = config.family?.familyId;\n if (familyId !== \"idb\") {\n throw new CliStructuredError(\n \"IDB-CLI.FAMILY_MISMATCH\",\n `config family is \"${familyId ?? \"unknown\"}\", expected \"idb\".`,\n {\n why: \"This CLI's migration commands operate on IDB-family projects only.\",\n nextActions: [\n {\n kind: \"edit-file\",\n label:\n \"Point --config at your browser-side prisma.config.ts (the one using @prisma-idb/family-idb/control)\",\n },\n ],\n }\n );\n }\n}\n\n/**\n * `defineCommand` pre-bound to the `orm` config section (see\n * `config-section.ts`) with a uniform error boundary, mirroring\n * `@prisma/orm-toolchain`'s own `defineOrmCommand`. The section's schema\n * resolves `contract.output`/`migrations.dir` against the config file that\n * wrote them (and defaults `migrations.dir`), so handlers always read\n * absolute paths without a separate finalize step.\n */\nexport function defineIdbCommand<\n TFlags extends Record<string, FlagSpec<unknown>> = Record<never, FlagSpec<unknown>>,\n TPositionals extends Record<string, PositionalSpec<unknown>> = Record<never, PositionalSpec<unknown>>,\n TCode extends number = never,\n>(\n def: {\n readonly help: HelpSpec;\n readonly args?: ArgsSpec<TFlags, TPositionals>;\n readonly exitCodes?: Readonly<Record<TCode, string>>;\n readonly handler: Handler<TFlags, TPositionals, PrismaNextConfig, TCode, false, false>;\n } & SpawnDeclarations\n): CommandDefinition<TFlags, TPositionals, PrismaNextConfig, TCode, false, false> {\n return defineCommand<TFlags, TPositionals, PrismaNextConfig, TCode, false, false>({\n ...def,\n needs: { config: ormConfigSection },\n handler: async (args, ctx) => {\n try {\n requireIdbFamily(ctx.config);\n return await def.handler(args, ctx);\n } catch (error) {\n return notOk(normalizeError(error));\n }\n },\n });\n}\n","import type { PrismaNextConfig } from \"@prisma/orm-toolchain/config-loader\";\nimport { CliStructuredError } from \"@prisma/cli-engine/protocol\";\nimport { resolve } from \"pathe\";\n\n/**\n * Resolves the contract/migrations paths a command needs from the already\n * finalized (absolute-paths) `orm` config section, with CLI flag overrides\n * winning when supplied. `ctx.config` arrives finalized by\n * `defineIdbCommand` (see `../define-command.ts`), so `config.contract.output`\n * and `config.migrations.dir` are already absolute — only an explicit\n * `--contract`/`--migrations-dir` flag (relative to `cwd`) needs resolving\n * here.\n */\nexport function requireContractPath(config: PrismaNextConfig, cwd: string, override: string | undefined): string {\n if (override !== undefined) return resolve(cwd, override);\n const output = config.contract?.output;\n if (output === undefined) {\n throw new CliStructuredError(\n \"IDB-CLI.CONTRACT_PATH_MISSING\",\n \"config.contract.output is required to resolve the contract path.\",\n {\n why: \"Neither --contract nor prisma.config.ts's orm.contract.output supplied a path.\",\n nextActions: [\n { kind: \"edit-file\", label: \"Set contract.output in prisma.config.ts, or pass --contract explicitly\" },\n ],\n }\n );\n }\n return output;\n}\n\n/**\n * `finalizeConfig` always resolves `migrations.dir` to an absolute path\n * (defaulting to `<configDir>/migrations` when unset); the fallback here is\n * defensive, not the expected path.\n */\nexport function resolveMigrationsDir(config: PrismaNextConfig, cwd: string, override: string | undefined): string {\n if (override !== undefined) return resolve(cwd, override);\n return config.migrations?.dir ?? resolve(cwd, \"migrations\");\n}\n","import { dirname, join, resolve } from \"pathe\";\nimport { flag } from \"@prisma/cli-engine\";\nimport { CliStructuredError, notOk, ok } from \"@prisma/cli-engine/protocol\";\nimport { createCollectingSink, presentationsFromSink } from \"../collecting-sink\";\nimport { defineIdbCommand } from \"../define-command\";\nimport { requireContractPath, resolveMigrationsDir } from \"./paths\";\n\nexport const migrationContractSpaceCommand = defineIdbCommand({\n help: {\n summary: \"Regenerate contract-space.generated.ts from migrations/app/\",\n description:\n \"Reads every migration package under <migrationsDir>/app/, validates the\\n\" +\n \"chain's connectivity, and emits a generated TypeScript module that bundles\\n\" +\n \"them into a ContractSpace.\",\n examples: [\"migration contract-space\", \"migration contract-space --out src/lib/prisma/contract-space.ts\"],\n },\n args: {\n flags: {\n contract: flag.string({\n brief: \"Path to contract.json (default: orm.contract.output)\",\n placeholder: \"path\",\n }),\n migrationsDir: flag.string({\n brief: \"Path to the migrations root (default: orm.migrations.dir)\",\n placeholder: \"path\",\n }),\n out: flag.string({\n brief: \"Output file path (default: colocated with the resolved contract.json)\",\n placeholder: \"path\",\n }),\n },\n },\n handler: async (args, ctx) => {\n const contractPath = requireContractPath(ctx.config, ctx.cwd, args.flags.contract);\n const migrationsDir = resolveMigrationsDir(ctx.config, ctx.cwd, args.flags.migrationsDir);\n const outPath =\n args.flags.out !== undefined\n ? resolve(ctx.cwd, args.flags.out)\n : join(dirname(contractPath), \"contract-space.generated.ts\");\n\n const { generateContractSpace } = await import(\"../../core/contract-space-codegen\");\n const sink = createCollectingSink();\n const exitCode = await generateContractSpace({\n migrationsDir,\n contractPath,\n outPath,\n out: sink.out,\n err: sink.err,\n });\n\n if (exitCode !== 0) {\n return notOk(\n new CliStructuredError(\"IDB-CLI.CONTRACT_SPACE_FAILED\", sink.fullText() || \"migration contract-space failed.\")\n );\n }\n return ok(ctx.present({ data: { exitCode, outPath } }, presentationsFromSink(sink, exitCode)));\n },\n});\n","import { flag } from \"@prisma/cli-engine\";\nimport { CliStructuredError, notOk, ok } from \"@prisma/cli-engine/protocol\";\nimport { createCollectingSink, presentationsFromSink } from \"../collecting-sink\";\nimport { defineIdbCommand } from \"../define-command\";\nimport { requireContractPath, resolveMigrationsDir } from \"./paths\";\n\nexport const migrationPlanCommand = defineIdbCommand({\n help: {\n summary: \"Plan the next IDB migration package (auto-detects baseline vs. incremental)\",\n description:\n \"Compares the emitted contract against the latest on-disk migration state and\\n\" +\n \"produces a new migration package with the required operations. Offline —\\n\" +\n \"never opens IndexedDB (there is no live database on the Node side to consult).\",\n examples: [\"migration plan\", \"migration plan --name add-posts\", \"migration plan --space idb-sync\"],\n },\n args: {\n flags: {\n name: flag.string({\n brief: 'Directory slug (default: \"baseline\" for a fresh space; required otherwise)',\n placeholder: \"slug\",\n }),\n space: flag.string({\n brief: 'Contract-space id (default: \"app\"). Only extension authors set this.',\n placeholder: \"id\",\n }),\n contract: flag.string({\n brief: \"Path to contract.json (default: orm.contract.output)\",\n placeholder: \"path\",\n }),\n migrationsDir: flag.string({\n brief: \"Path to the migrations root (default: orm.migrations.dir)\",\n placeholder: \"path\",\n }),\n },\n },\n handler: async (args, ctx) => {\n const contractPath = requireContractPath(ctx.config, ctx.cwd, args.flags.contract);\n const migrationsDir = resolveMigrationsDir(ctx.config, ctx.cwd, args.flags.migrationsDir);\n\n const { migrationPlan } = await import(\"../../core/migration-plan\");\n const sink = createCollectingSink();\n const exitCode = await migrationPlan({\n migrationsDir,\n contractPath,\n out: sink.out,\n err: sink.err,\n ...(args.flags.name !== undefined && { name: args.flags.name }),\n ...(args.flags.space !== undefined && { spaceId: args.flags.space }),\n });\n\n if (exitCode === 2) {\n return notOk(\n new CliStructuredError(\n \"IDB-CLI.MIGRATION_NAME_REQUIRED\",\n sink.fullText() || \"--name <slug> is required when generating an incremental migration.\"\n )\n );\n }\n if (exitCode !== 0) {\n return notOk(\n new CliStructuredError(\"IDB-CLI.MIGRATION_PLAN_FAILED\", sink.fullText() || \"migration plan failed.\")\n );\n }\n return ok(ctx.present({ data: { exitCode } }, presentationsFromSink(sink, exitCode)));\n },\n});\n","import { flag } from \"@prisma/cli-engine\";\nimport { CliStructuredError, notOk, ok } from \"@prisma/cli-engine/protocol\";\nimport { createCollectingSink, presentationsFromSink } from \"../collecting-sink\";\nimport { defineIdbCommand } from \"../define-command\";\nimport { resolveMigrationsDir } from \"./paths\";\n\nexport const migrationPreflightCommand = defineIdbCommand({\n help: {\n summary: \"Validate the migration chain against fake-indexeddb\",\n description:\n \"Walks every migration package under <migrationsDir>/app/ in chain order,\\n\" +\n \"applying each package's ops.json against a fresh fake-indexeddb instance.\\n\" +\n \"Verifies the final stores and indexes against the head contract snapshot.\\n\" +\n \"Fails on replay errors, schema drift, or a missing or mismatched head snapshot.\",\n examples: [\"migration preflight\"],\n },\n args: {\n flags: {\n migrationsDir: flag.string({\n brief: \"Path to the migrations root (default: orm.migrations.dir)\",\n placeholder: \"path\",\n }),\n },\n },\n handler: async (args, ctx) => {\n const migrationsDir = resolveMigrationsDir(ctx.config, ctx.cwd, args.flags.migrationsDir);\n\n const { runPreflight } = await import(\"../../core/preflight\");\n const sink = createCollectingSink();\n const exitCode = await runPreflight({ migrationsDir, out: sink.out, err: sink.err });\n\n if (exitCode !== 0) {\n return notOk(new CliStructuredError(\"IDB-CLI.PREFLIGHT_FAILED\", sink.fullText() || \"Preflight failed.\"));\n }\n return ok(ctx.present({ data: { exitCode } }, presentationsFromSink(sink, exitCode)));\n },\n});\n","import type { AnyCommand } from \"@prisma/cli-engine\";\nimport { defineCommandFamily } from \"@prisma/cli-engine\";\nimport { ormConfigSection } from \"./config-section\";\nimport { migrationContractSpaceCommand } from \"./migration/contract-space\";\nimport { migrationPlanCommand } from \"./migration/plan\";\nimport { migrationPreflightCommand } from \"./migration/preflight\";\n\nconst commands: Readonly<Record<string, AnyCommand>> = {\n \"migration plan\": migrationPlanCommand,\n \"migration contract-space\": migrationContractSpaceCommand,\n \"migration preflight\": migrationPreflightCommand,\n};\n\n/**\n * The unit this package contributes to a `@prisma/cli-engine` shell: its\n * config section (reused from the ORM family, see `config-section.ts`) and\n * its 3 commands. These have no generic equivalent — the real `prisma` CLI's\n * `migration new`/`plan`/etc. work against a live database or a SQL/Mongo\n * on-disk model; ours works against `fake-indexeddb` and a hand-rolled\n * contract-space convention.\n */\nexport const idbCommandFamily = defineCommandFamily({\n configSection: ormConfigSection,\n commands,\n});\n","","import type { Cli, HostProcess, Runtime } from \"@prisma/cli-engine\";\nimport { createCli, loadConfig } from \"@prisma/cli-engine\";\nimport { CliStructuredError } from \"@prisma/cli-engine/protocol\";\nimport packageJson from \"../../package.json\" with { type: \"json\" };\nimport { idbCommandFamily } from \"./family\";\n\nexport const BIN_NAME = \"prisma-idb\";\n\nexport const BIN_GROUPS = {\n migration: {\n brief: \"On-disk migration management commands\",\n description:\n \"Plan, bundle, and validate IDB migration packages. Migrations are\\n\" +\n \"contract-to-contract edges stored as versioned directories under migrations/.\",\n },\n} as const;\n\nexport function createIdbCli(): Cli {\n return createCli({\n name: BIN_NAME,\n version: packageJson.version,\n commandFamilies: [idbCommandFamily],\n groups: BIN_GROUPS,\n commands: idbCommandFamily.commands,\n help: {\n tagline: \"IDB-target tooling for Prisma 8\",\n description: \"Migration authoring for IndexedDB projects — no live database on the Node side to consult.\",\n },\n });\n}\n\n/** Everything environmental the engine is given, adapted from the host process once. */\nexport function runtimeFromProcess(proc: HostProcess): Runtime {\n return {\n stdout: { write: (text) => void proc.stdout.write(text) },\n stderr: { write: (text) => void proc.stderr.write(text) },\n stdin: proc.stdin,\n cwd: proc.cwd(),\n env: proc.env,\n isTty: {\n stdin: proc.stdin.isTTY === true,\n stdout: proc.stdout.isTTY === true,\n stderr: proc.stderr.isTTY === true,\n },\n host: {\n runtime: { name: \"node\", version: proc.version },\n platform: proc.platform,\n arch: proc.arch,\n },\n exit: (code) => proc.exit(code),\n onSignal: (callback) => {\n const onInterrupt = () => callback(\"SIGINT\");\n const onTerminate = () => callback(\"SIGTERM\");\n proc.on(\"SIGINT\", onInterrupt);\n proc.on(\"SIGTERM\", onTerminate);\n return () => {\n proc.off(\"SIGINT\", onInterrupt);\n proc.off(\"SIGTERM\", onTerminate);\n };\n },\n loadConfig: (configPath) => loadConfig(proc.cwd(), configPath),\n // Never reached: no command here declares `credentials`/`managesCredentials`,\n // so ctx.api is never constructed — required on Runtime regardless.\n managementApi: { baseUrl: \"https://api.prisma.io\" },\n };\n}\n\nconst STARTUP_FAILURE_EXIT_CODE = 1;\n\nfunction reportStartupFailure(proc: HostProcess, error: unknown): number {\n const normalized = CliStructuredError.is(error)\n ? error\n : new CliStructuredError(\"IDB-CLI.STARTUP_FAILURE\", error instanceof Error ? error.message : String(error));\n proc.stderr.write(`✘ [${normalized.code}] ${normalized.message}\\n`);\n return STARTUP_FAILURE_EXIT_CODE;\n}\n\n/** Parses, executes and settles one invocation; returns the exit code. */\nexport async function runIdbCli(proc: HostProcess): Promise<number> {\n try {\n return await createIdbCli().run(proc.argv.slice(2), runtimeFromProcess(proc));\n } catch (error) {\n return reportStartupFailure(proc, error);\n }\n}\n"],"mappings":"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"}
|
package/dist/exports/cli.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as idbCommandFamily, i as runIdbCli, n as BIN_NAME, r as createIdbCli, t as BIN_GROUPS } from "../cli-
|
|
1
|
+
import { a as idbCommandFamily, i as runIdbCli, n as BIN_NAME, r as createIdbCli, t as BIN_GROUPS } from "../cli-Dhspvkvy.mjs";
|
|
2
2
|
export { BIN_GROUPS, BIN_NAME, createIdbCli, idbCommandFamily, runIdbCli };
|
package/dist/exports/control.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { t as verifyIdbSchema } from "../schema-verify-B7-xiHem.mjs";
|
|
1
2
|
import { t as validateContract } from "../validate-dwKE5_5B.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { keyPathEquals } from "@prisma-idb/target-idb/pack";
|
|
3
|
+
import { VERIFY_CODE_TARGET_MISMATCH } from "@prisma/orm-framework/components/control";
|
|
4
4
|
//#region src/core/emission.ts
|
|
5
5
|
/**
|
|
6
6
|
* Converts a JavaScript primitive or plain object into a TypeScript type-literal
|
|
@@ -163,263 +163,6 @@ const idbEmission = {
|
|
|
163
163
|
}
|
|
164
164
|
};
|
|
165
165
|
//#endregion
|
|
166
|
-
//#region src/core/schema-verify.ts
|
|
167
|
-
function passNode(kind, name, contractPath, children = []) {
|
|
168
|
-
return {
|
|
169
|
-
status: "pass",
|
|
170
|
-
kind,
|
|
171
|
-
name,
|
|
172
|
-
contractPath,
|
|
173
|
-
code: "ok",
|
|
174
|
-
message: `${name} matches`,
|
|
175
|
-
expected: null,
|
|
176
|
-
actual: null,
|
|
177
|
-
children
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
function failNode(kind, name, contractPath, code, message, expected, actual, children = []) {
|
|
181
|
-
return {
|
|
182
|
-
status: "fail",
|
|
183
|
-
kind,
|
|
184
|
-
name,
|
|
185
|
-
contractPath,
|
|
186
|
-
code,
|
|
187
|
-
message,
|
|
188
|
-
expected,
|
|
189
|
-
actual,
|
|
190
|
-
children
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
function warnNode(kind, name, contractPath, code, message, expected, actual, children = []) {
|
|
194
|
-
return {
|
|
195
|
-
status: "warn",
|
|
196
|
-
kind,
|
|
197
|
-
name,
|
|
198
|
-
contractPath,
|
|
199
|
-
code,
|
|
200
|
-
message,
|
|
201
|
-
expected,
|
|
202
|
-
actual,
|
|
203
|
-
children
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
function countStatuses(nodes) {
|
|
207
|
-
let pass = 0, warn = 0, fail = 0, totalNodes = 0;
|
|
208
|
-
const stack = [...nodes];
|
|
209
|
-
while (stack.length > 0) {
|
|
210
|
-
const node = stack.pop();
|
|
211
|
-
totalNodes++;
|
|
212
|
-
if (node.status === "pass") pass++;
|
|
213
|
-
else if (node.status === "warn") warn++;
|
|
214
|
-
else fail++;
|
|
215
|
-
stack.push(...node.children);
|
|
216
|
-
}
|
|
217
|
-
return {
|
|
218
|
-
pass,
|
|
219
|
-
warn,
|
|
220
|
-
fail,
|
|
221
|
-
totalNodes
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
function verifyIndex(indexName, contractIndex, actualIndex, storeName, storePath, issues) {
|
|
225
|
-
const indexPath = `${storePath}.indexes.${indexName}`;
|
|
226
|
-
if (actualIndex === void 0) {
|
|
227
|
-
const msg = `Index "${indexName}" defined in contract is missing from manifest schema`;
|
|
228
|
-
issues.push({
|
|
229
|
-
kind: "missing_column",
|
|
230
|
-
table: storeName,
|
|
231
|
-
column: indexName,
|
|
232
|
-
message: msg,
|
|
233
|
-
path: [storeName, indexName]
|
|
234
|
-
});
|
|
235
|
-
return failNode("index", indexName, indexPath, "missing_column", msg, contractIndex, void 0);
|
|
236
|
-
}
|
|
237
|
-
const children = [];
|
|
238
|
-
if (!keyPathEquals(contractIndex.keyPath, actualIndex.keyPath)) {
|
|
239
|
-
const msg = `Index "${indexName}" keyPath mismatch: expected ${JSON.stringify(contractIndex.keyPath)}, got ${JSON.stringify(actualIndex.keyPath)}`;
|
|
240
|
-
issues.push({
|
|
241
|
-
kind: "index_mismatch",
|
|
242
|
-
table: storeName,
|
|
243
|
-
indexOrConstraint: indexName,
|
|
244
|
-
message: msg,
|
|
245
|
-
path: [storeName, indexName]
|
|
246
|
-
});
|
|
247
|
-
children.push(failNode("field", "keyPath", `${indexPath}.keyPath`, "index_mismatch", msg, contractIndex.keyPath, actualIndex.keyPath));
|
|
248
|
-
} else children.push(passNode("field", "keyPath", `${indexPath}.keyPath`));
|
|
249
|
-
if ((contractIndex.unique ?? false) !== (actualIndex.unique ?? false)) {
|
|
250
|
-
const msg = `Index "${indexName}" unique mismatch: expected ${contractIndex.unique}, got ${actualIndex.unique}`;
|
|
251
|
-
issues.push({
|
|
252
|
-
kind: "index_mismatch",
|
|
253
|
-
table: storeName,
|
|
254
|
-
indexOrConstraint: indexName,
|
|
255
|
-
message: msg,
|
|
256
|
-
path: [storeName, indexName]
|
|
257
|
-
});
|
|
258
|
-
children.push(failNode("field", "unique", `${indexPath}.unique`, "index_mismatch", msg, contractIndex.unique, actualIndex.unique));
|
|
259
|
-
} else children.push(passNode("field", "unique", `${indexPath}.unique`));
|
|
260
|
-
const contractME = contractIndex.multiEntry ?? false;
|
|
261
|
-
const actualME = actualIndex.multiEntry ?? false;
|
|
262
|
-
if (contractME !== actualME) {
|
|
263
|
-
const msg = `Index "${indexName}" multiEntry mismatch: expected ${contractME}, got ${actualME}`;
|
|
264
|
-
issues.push({
|
|
265
|
-
kind: "index_mismatch",
|
|
266
|
-
table: storeName,
|
|
267
|
-
indexOrConstraint: indexName,
|
|
268
|
-
message: msg,
|
|
269
|
-
path: [storeName, indexName]
|
|
270
|
-
});
|
|
271
|
-
children.push(failNode("field", "multiEntry", `${indexPath}.multiEntry`, "index_mismatch", msg, contractME, actualME));
|
|
272
|
-
} else if (contractME) children.push(passNode("field", "multiEntry", `${indexPath}.multiEntry`));
|
|
273
|
-
if (children.some((c) => c.status === "fail")) return failNode("index", indexName, indexPath, "index_mismatch", `Index "${indexName}" has mismatches`, null, null, children);
|
|
274
|
-
return passNode("index", indexName, indexPath, children);
|
|
275
|
-
}
|
|
276
|
-
function verifyStore(storeName, contractStore, actualStore, strict, issues) {
|
|
277
|
-
const storePath = `storage.stores.${storeName}`;
|
|
278
|
-
if (actualStore === void 0) {
|
|
279
|
-
const msg = `Object store "${storeName}" defined in contract is missing from manifest schema`;
|
|
280
|
-
issues.push({
|
|
281
|
-
kind: "missing_table",
|
|
282
|
-
table: storeName,
|
|
283
|
-
message: msg,
|
|
284
|
-
path: [storeName]
|
|
285
|
-
});
|
|
286
|
-
return failNode("collection", storeName, storePath, "missing_table", msg, contractStore, void 0);
|
|
287
|
-
}
|
|
288
|
-
const children = [];
|
|
289
|
-
if (!keyPathEquals(contractStore.keyPath, actualStore.keyPath)) {
|
|
290
|
-
const msg = `Store "${storeName}" keyPath mismatch: expected ${JSON.stringify(contractStore.keyPath)}, got ${JSON.stringify(actualStore.keyPath)}`;
|
|
291
|
-
issues.push({
|
|
292
|
-
kind: "primary_key_mismatch",
|
|
293
|
-
table: storeName,
|
|
294
|
-
message: msg,
|
|
295
|
-
path: [storeName]
|
|
296
|
-
});
|
|
297
|
-
children.push(failNode("field", "keyPath", `${storePath}.keyPath`, "primary_key_mismatch", msg, contractStore.keyPath, actualStore.keyPath));
|
|
298
|
-
} else children.push(passNode("field", "keyPath", `${storePath}.keyPath`));
|
|
299
|
-
const contractAI = contractStore.autoIncrement ?? false;
|
|
300
|
-
const actualAI = actualStore.autoIncrement ?? false;
|
|
301
|
-
if (contractAI !== actualAI) {
|
|
302
|
-
const msg = `Store "${storeName}" autoIncrement mismatch: expected ${contractAI}, got ${actualAI}`;
|
|
303
|
-
issues.push({
|
|
304
|
-
kind: "type_mismatch",
|
|
305
|
-
table: storeName,
|
|
306
|
-
message: msg,
|
|
307
|
-
path: [storeName]
|
|
308
|
-
});
|
|
309
|
-
children.push(failNode("field", "autoIncrement", `${storePath}.autoIncrement`, "type_mismatch", msg, contractAI, actualAI));
|
|
310
|
-
} else if (contractAI) children.push(passNode("field", "autoIncrement", `${storePath}.autoIncrement`));
|
|
311
|
-
const contractIndexes = contractStore.indexes ?? {};
|
|
312
|
-
const actualIndexes = actualStore.indexes ?? {};
|
|
313
|
-
for (const [indexName, contractIndex] of Object.entries(contractIndexes)) {
|
|
314
|
-
const actualIndex = actualIndexes[indexName];
|
|
315
|
-
children.push(verifyIndex(indexName, contractIndex, actualIndex, storeName, storePath, issues));
|
|
316
|
-
}
|
|
317
|
-
for (const indexName of Object.keys(actualIndexes)) if (!(indexName in contractIndexes)) {
|
|
318
|
-
const msg = `Index "${indexName}" exists in manifest but is not in contract`;
|
|
319
|
-
if (strict) {
|
|
320
|
-
issues.push({
|
|
321
|
-
kind: "extra_index",
|
|
322
|
-
table: storeName,
|
|
323
|
-
indexOrConstraint: indexName,
|
|
324
|
-
message: msg,
|
|
325
|
-
path: [storeName, indexName]
|
|
326
|
-
});
|
|
327
|
-
children.push(failNode("index", indexName, `${storePath}.indexes.${indexName}`, "extra_index", msg, void 0, actualIndexes[indexName]));
|
|
328
|
-
} else children.push(warnNode("index", indexName, `${storePath}.indexes.${indexName}`, "extra_index", msg, void 0, actualIndexes[indexName]));
|
|
329
|
-
}
|
|
330
|
-
if (children.some((c) => c.status === "fail")) return failNode("collection", storeName, storePath, "schema_mismatch", `Store "${storeName}" has schema issues`, null, null, children);
|
|
331
|
-
if (children.some((c) => c.status === "warn")) return {
|
|
332
|
-
status: "warn",
|
|
333
|
-
kind: "collection",
|
|
334
|
-
name: storeName,
|
|
335
|
-
contractPath: storePath,
|
|
336
|
-
code: "ok",
|
|
337
|
-
message: `Store "${storeName}" has warnings`,
|
|
338
|
-
expected: null,
|
|
339
|
-
actual: null,
|
|
340
|
-
children
|
|
341
|
-
};
|
|
342
|
-
return passNode("collection", storeName, storePath, children);
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Pure, synchronous schema verification for IndexedDB.
|
|
346
|
-
*
|
|
347
|
-
* Compares the stores and indexes defined in the contract against the manifest
|
|
348
|
-
* schema (which represents the actual IndexedDB database). Returns a
|
|
349
|
-
* {@link VerifyDatabaseSchemaResult} that callers can inspect for issues.
|
|
350
|
-
*
|
|
351
|
-
* Does not perform any I/O — all inputs must already be available.
|
|
352
|
-
*
|
|
353
|
-
* @param contract - Validated IDB contract (what we expect the schema to be).
|
|
354
|
-
* @param schema - Schema IR from the manifest (what the schema actually is).
|
|
355
|
-
* @param strict - When `true`, extra stores/indexes in the manifest are failures.
|
|
356
|
-
* @param options - Optional context paths for metadata / error reporting.
|
|
357
|
-
*/
|
|
358
|
-
function verifyIdbSchema(contract, schema, strict) {
|
|
359
|
-
const start = Date.now();
|
|
360
|
-
const issues = [];
|
|
361
|
-
const storeNodes = [];
|
|
362
|
-
const contractStores = contract.storage.stores;
|
|
363
|
-
const actualStores = schema.stores;
|
|
364
|
-
for (const [storeName, contractStore] of Object.entries(contractStores)) {
|
|
365
|
-
const actualStore = actualStores[storeName];
|
|
366
|
-
storeNodes.push(verifyStore(storeName, contractStore, actualStore, strict, issues));
|
|
367
|
-
}
|
|
368
|
-
for (const [storeName, actualStore] of Object.entries(actualStores)) if (!(storeName in contractStores)) {
|
|
369
|
-
const msg = `Object store "${storeName}" exists in manifest but is not in contract`;
|
|
370
|
-
if (strict) {
|
|
371
|
-
issues.push({
|
|
372
|
-
kind: "extra_table",
|
|
373
|
-
table: storeName,
|
|
374
|
-
message: msg,
|
|
375
|
-
path: [storeName]
|
|
376
|
-
});
|
|
377
|
-
storeNodes.push(failNode("collection", storeName, `storage.stores.${storeName}`, "extra_table", msg, void 0, actualStore));
|
|
378
|
-
} else storeNodes.push(warnNode("collection", storeName, `storage.stores.${storeName}`, "extra_table", msg, void 0, actualStore));
|
|
379
|
-
}
|
|
380
|
-
const root = {
|
|
381
|
-
status: storeNodes.some((n) => n.status === "fail") ? "fail" : storeNodes.some((n) => n.status === "warn") ? "warn" : "pass",
|
|
382
|
-
kind: "root",
|
|
383
|
-
name: "idb",
|
|
384
|
-
contractPath: "storage.stores",
|
|
385
|
-
code: issues.length === 0 ? "ok" : "schema_mismatch",
|
|
386
|
-
message: issues.length === 0 ? "All stores and indexes match" : `${issues.length} schema issue(s) found`,
|
|
387
|
-
expected: null,
|
|
388
|
-
actual: null,
|
|
389
|
-
children: storeNodes
|
|
390
|
-
};
|
|
391
|
-
const counts = countStatuses([root, ...storeNodes]);
|
|
392
|
-
const ok = root.status !== "fail";
|
|
393
|
-
const storageHash = contract.storage.storageHash;
|
|
394
|
-
const profileHash = contract.profileHash;
|
|
395
|
-
const contractField = profileHash !== void 0 ? {
|
|
396
|
-
storageHash,
|
|
397
|
-
profileHash
|
|
398
|
-
} : { storageHash };
|
|
399
|
-
const metaField = { strict };
|
|
400
|
-
const sharedResult = {
|
|
401
|
-
summary: ok ? "Schema verification passed" : `Schema verification failed: ${issues.length} issue(s)`,
|
|
402
|
-
contract: contractField,
|
|
403
|
-
target: { expected: "idb" },
|
|
404
|
-
schema: {
|
|
405
|
-
issues,
|
|
406
|
-
root,
|
|
407
|
-
counts
|
|
408
|
-
},
|
|
409
|
-
meta: metaField,
|
|
410
|
-
timings: { total: Date.now() - start }
|
|
411
|
-
};
|
|
412
|
-
if (ok) return {
|
|
413
|
-
ok: true,
|
|
414
|
-
...sharedResult
|
|
415
|
-
};
|
|
416
|
-
return {
|
|
417
|
-
ok: false,
|
|
418
|
-
code: VERIFY_CODE_SCHEMA_FAILURE,
|
|
419
|
-
...sharedResult
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
//#endregion
|
|
423
166
|
//#region src/core/control-instance.ts
|
|
424
167
|
const REFUSAL_CODE = "IDB-CLI-UNSUPPORTED";
|
|
425
168
|
/**
|
|
@@ -505,7 +248,8 @@ function createIdbFamilyInstance(_stack) {
|
|
|
505
248
|
};
|
|
506
249
|
},
|
|
507
250
|
verifySchema(options) {
|
|
508
|
-
|
|
251
|
+
const contract = validateContract(options.contract);
|
|
252
|
+
return verifyIdbSchema(contract, options.schema, options.strict);
|
|
509
253
|
},
|
|
510
254
|
async sign(options) {
|
|
511
255
|
const start = Date.now();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control.mjs","names":[],"sources":["../../src/core/emission.ts","../../src/core/schema-verify.ts","../../src/core/control-instance.ts","../../src/core/control-descriptor.ts","../../src/exports/control.ts"],"sourcesContent":["import type { Contract, ContractModel } from \"@prisma/orm-framework/contract/types\";\nimport type { EmissionSpi } from \"@prisma/orm-framework/components/emission\";\nimport type { IdbModelStorage, IdbStorage } from \"@prisma-idb/target-idb/pack\";\n\n// ── Serialization utilities ──────────────────────────────────────────────────\n// Adapted from Prisma ORM's domain type generation.\n// These utilities convert runtime JS values into TypeScript type-literal strings\n// that are spliced verbatim into the generated contract.d.ts file.\n\n/**\n * Converts a JavaScript primitive or plain object into a TypeScript type-literal\n * string. Arrays become `readonly [...]` tuples; objects become `{ readonly k: v }`\n * type literals; strings are single-quoted with escaping.\n */\nfunction serializeValue(value: unknown): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"string\") {\n const escaped = value.replace(/\\\\/g, \"\\\\\\\\\").replace(/'/g, \"\\\\'\");\n return `'${escaped}'`;\n }\n if (typeof value === \"number\" || typeof value === \"boolean\") return String(value);\n if (typeof value === \"bigint\") return `${value}n`;\n if (Array.isArray(value)) {\n const items = value.map((v) => serializeValue(v)).join(\", \");\n return `readonly [${items}]`;\n }\n if (typeof value === \"object\") {\n const entries: string[] = [];\n for (const [k, v] of Object.entries(value)) {\n entries.push(`readonly ${serializeObjectKey(k)}: ${serializeValue(v)}`);\n }\n return `{ ${entries.join(\"; \")} }`;\n }\n return \"unknown\";\n}\n\n/**\n * Converts an object key to a safe TypeScript identifier. If the key is already a\n * valid identifier it is returned as-is; otherwise it is wrapped in single-quoted\n * string literal form.\n */\nfunction serializeObjectKey(key: string): string {\n if (/^[$A-Z_a-z][$\\w]*$/.test(key)) return key;\n return serializeValue(key);\n}\n\n// ── EmissionSpi ───────────────────────────────────────────────────────────────\n\n/**\n * IDB family emission plugin.\n *\n * Registered as `emission` on {@link IdbFamilyDescriptor}. The Prisma 8 emitter\n * calls these methods when generating `contract.d.ts` for any schema that targets the\n * IDB family. Each method returns a raw TypeScript source string that the emitter\n * splices into the generated file at the appropriate location.\n *\n * @see `generateContractDts` in `@prisma/orm-toolchain`.\n */\nexport const idbEmission = {\n id: \"idb\",\n\n /**\n * Serializes the full `storage` section of the contract into a TypeScript type\n * literal. The result is used as the first generic argument of `ContractType<...>`\n * in the generated `contract.d.ts`.\n *\n * For IDB the storage shape is:\n * ```ts\n * { readonly stores: { readonly <storeName>: { readonly keyPath: '...'; ... }; ... };\n * readonly storageHash: <storageHashTypeName> }\n * ```\n *\n * @param contract - The resolved Prisma contract.\n * @param storageHashTypeName - Emitter-provided name of the `StorageHash` type alias\n * to embed (e.g. `'StorageHash'`).\n */\n generateStorageType(contract: Contract, storageHashTypeName: string): string {\n const storage = contract.storage as IdbStorage;\n const stores: string[] = [];\n\n for (const [storeName, store] of Object.entries(storage.stores).sort(([a], [b]) => a.localeCompare(b))) {\n const storeParts: string[] = [`readonly keyPath: ${serializeValue(store.keyPath)}`];\n\n if (store.autoIncrement !== undefined) {\n storeParts.push(`readonly autoIncrement: ${store.autoIncrement}`);\n }\n\n const indexes = store.indexes ?? {};\n if (Object.keys(indexes).length > 0) {\n const indexEntries: string[] = [];\n for (const [indexName, index] of Object.entries(indexes).sort(([a], [b]) => a.localeCompare(b))) {\n // Canonicalization drops `unique: false`, so an absent flag means a non-unique index.\n const indexParts = [\n `readonly keyPath: ${serializeValue(index.keyPath)}`,\n `readonly unique: ${index.unique ?? false}`,\n ];\n if (index.multiEntry !== undefined) {\n indexParts.push(`readonly multiEntry: ${index.multiEntry}`);\n }\n indexEntries.push(`readonly ${serializeObjectKey(indexName)}: { ${indexParts.join(\"; \")} }`);\n }\n storeParts.push(`readonly indexes: { ${indexEntries.join(\"; \")} }`);\n } else {\n storeParts.push(`readonly indexes: Record<string, never>`);\n }\n\n stores.push(`readonly ${serializeObjectKey(storeName)}: { ${storeParts.join(\"; \")} }`);\n }\n\n const storesType = stores.length > 0 ? `{ ${stores.join(\"; \")} }` : \"Record<string, never>\";\n\n // v0.12.0: StorageBase now requires `namespaces`. Generate the literal type from\n // the contract's storage namespaces so the emitted storage type satisfies StorageBase.\n const nsEntries: string[] = [];\n for (const [nsId, ns] of Object.entries(\n (storage as unknown as { namespaces?: Record<string, { id: string; entries?: Record<string, unknown> }> })\n .namespaces ?? {}\n ).sort(([a], [b]) => a.localeCompare(b))) {\n const entriesObj = ns.entries ?? {};\n const entriesType = Object.keys(entriesObj).length === 0 ? \"Record<string, never>\" : serializeValue(entriesObj);\n nsEntries.push(\n `readonly ${serializeObjectKey(nsId)}: { readonly id: ${serializeValue(ns.id)}; readonly entries: ${entriesType} }`\n );\n }\n const namespacesType = nsEntries.length > 0 ? `{ ${nsEntries.join(\"; \")} }` : \"Record<string, never>\";\n\n return `{ readonly stores: ${storesType}; readonly namespaces: ${namespacesType}; readonly storageHash: ${storageHashTypeName} }`;\n },\n\n /**\n * Serializes the per-model storage metadata into a TypeScript type literal.\n * The result is used as the `storage` field of the model's entry in the\n * generated `models` type.\n *\n * For IDB, each model maps to exactly one object store:\n * ```ts\n * { readonly storeName: '<name>'; readonly keyPath: '<field>' }\n * ```\n *\n * @param _modelName - The Prisma model name (unused; provided for API symmetry).\n * @param model - The resolved `ContractModel` whose `storage` is cast to\n * {@link IdbModelStorage}.\n */\n generateModelStorageType(_modelName: string, model: ContractModel): string {\n const idbModel = model as ContractModel<IdbModelStorage>;\n return [\n `{ readonly storeName: ${serializeValue(idbModel.storage.storeName)}`,\n `readonly keyPath: ${serializeValue(idbModel.storage.keyPath)} }`,\n ].join(\"; \");\n },\n\n resolveFieldValueSet(_modelName: string, fieldName: string, model: ContractModel, contract: Contract) {\n const ref = model.fields[fieldName]?.valueSet;\n if (ref?.plane !== \"domain\" || ref.entityKind !== \"enum\" || ref.spaceId !== undefined) return undefined;\n const entry = contract.domain.namespaces[ref.namespaceId]?.enum?.[ref.entityName];\n if (entry === undefined) return undefined;\n return {\n codecId: entry.codecId,\n encodedValues: entry.members.map((member) => member.value),\n };\n },\n\n /**\n * Returns the `import type` lines prepended to `contract.d.ts` that bring\n * IDB-family types into scope.\n *\n * Imports {@link IdbContractWithTypeMaps} (the phantom wrapper used by\n * {@link getContractWrapper}) and {@link IdbTypeMaps} (used by\n * {@link getTypeMapsExpression}).\n */\n getFamilyImports(): string[] {\n return [\"import type { IdbContractWithTypeMaps, IdbTypeMaps } from '@prisma-idb/target-idb/pack';\"];\n },\n\n /**\n * Returns additional type alias declarations appended to `contract.d.ts`\n * after the codec and operation type aliases.\n *\n * For IDB this exports `LaneCodecTypes` (used by the generated client to\n * look up codec input/output pairs) as a simple alias of the assembled\n * `CodecTypes`. IDB has no query-operation types, so no further aliases\n * are needed.\n */\n getFamilyTypeAliases(): string {\n return \"export type LaneCodecTypes = CodecTypes;\";\n },\n\n /**\n * Returns the TypeScript expression for the `TypeMaps<...>` generic\n * instantiation written into `contract.d.ts`.\n *\n * IDB has no operation types or query-operation types, so the expression\n * uses the three-argument form of {@link IdbTypeMaps}.\n */\n getTypeMapsExpression(): string {\n return \"IdbTypeMaps<CodecTypes, FieldOutputTypes, FieldInputTypes>\";\n },\n\n /**\n * Returns the final `export type Contract = ...` declaration and convenience\n * re-exports derived from it.\n *\n * @param contractBaseName - Emitter-provided name of the base contract type\n * (always `'ContractBase'`).\n * @param typeMapsName - Emitter-provided name of the assembled TypeMaps type\n * (always `'TypeMaps'`).\n */\n getContractWrapper(contractBaseName: string, typeMapsName: string): string {\n return [\n `export type Contract = IdbContractWithTypeMaps<${contractBaseName}, ${typeMapsName}>;`,\n \"\",\n \"export type Stores = Contract['storage']['stores'];\",\n ].join(\"\\n\");\n },\n} as const satisfies EmissionSpi;\n","import {\n VERIFY_CODE_SCHEMA_FAILURE,\n type SchemaDiffIssue,\n type VerifyDatabaseSchemaResult,\n} from \"@prisma/orm-framework/components/control\";\nimport { keyPathEquals } from \"@prisma-idb/target-idb/pack\";\nimport type { IdbContract } from \"./validate\";\nimport type { IdbIndexIR, IdbSchemaIR, IdbStoreIR } from \"./schema-ir\";\n\n/**\n * IDB's own presentation tree for `db verify` output. The framework's\n * `VerifyDatabaseSchemaResult` (0.16.0, TML-3028/#921/#992) dropped the\n * shared verification-tree vocabulary (`SchemaVerificationNode`) in favor of\n * a flat `SchemaDiffIssue[]` produced by a generic `DiffableNode` tree\n * differ. IDB's schema is only two levels deep (store → index), so adopting\n * that generic differ isn't worth the ceremony — this keeps the richer\n * pass/warn/fail tree as an IDB-local addition on top of the (still\n * satisfied) `schema.issues` contract.\n */\ninterface SchemaVerificationNode {\n readonly status: \"pass\" | \"warn\" | \"fail\";\n readonly kind: string;\n readonly name: string;\n readonly contractPath: string;\n readonly code: string;\n readonly message: string;\n readonly expected: unknown;\n readonly actual: unknown;\n readonly children: readonly SchemaVerificationNode[];\n}\n\n/** IDB's issue shape, extended with `path` so it satisfies `SchemaDiffIssue`. */\ntype SchemaIssue = SchemaDiffIssue & {\n readonly kind: string;\n readonly table: string;\n readonly column?: string;\n readonly indexOrConstraint?: string;\n readonly message: string;\n};\n\n/**\n * `verifyIdbSchema`'s actual return shape: a strict superset of\n * `VerifyDatabaseSchemaResult` that keeps the pass/warn/fail tree and node\n * counts as IDB-local additions. Assignable anywhere a plain\n * `VerifyDatabaseSchemaResult` is expected (see `control-instance.ts`).\n */\nexport interface IdbVerifyDatabaseSchemaResult extends Omit<VerifyDatabaseSchemaResult, \"schema\"> {\n readonly schema: {\n readonly issues: readonly SchemaIssue[];\n readonly warnings?: { readonly issues: readonly SchemaIssue[] };\n readonly root: SchemaVerificationNode;\n readonly counts: {\n readonly pass: number;\n readonly warn: number;\n readonly fail: number;\n readonly totalNodes: number;\n };\n };\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction passNode(\n kind: string,\n name: string,\n contractPath: string,\n children: readonly SchemaVerificationNode[] = []\n): SchemaVerificationNode {\n return {\n status: \"pass\",\n kind,\n name,\n contractPath,\n code: \"ok\",\n message: `${name} matches`,\n expected: null,\n actual: null,\n children,\n };\n}\n\nfunction failNode(\n kind: string,\n name: string,\n contractPath: string,\n code: string,\n message: string,\n expected: unknown,\n actual: unknown,\n children: readonly SchemaVerificationNode[] = []\n): SchemaVerificationNode {\n return {\n status: \"fail\",\n kind,\n name,\n contractPath,\n code,\n message,\n expected,\n actual,\n children,\n };\n}\n\nfunction warnNode(\n kind: string,\n name: string,\n contractPath: string,\n code: string,\n message: string,\n expected: unknown,\n actual: unknown,\n children: readonly SchemaVerificationNode[] = []\n): SchemaVerificationNode {\n return {\n status: \"warn\",\n kind,\n name,\n contractPath,\n code,\n message,\n expected,\n actual,\n children,\n };\n}\n\nfunction countStatuses(nodes: readonly SchemaVerificationNode[]): {\n pass: number;\n warn: number;\n fail: number;\n totalNodes: number;\n} {\n let pass = 0,\n warn = 0,\n fail = 0,\n totalNodes = 0;\n const stack = [...nodes];\n while (stack.length > 0) {\n const node = stack.pop()!;\n totalNodes++;\n if (node.status === \"pass\") pass++;\n else if (node.status === \"warn\") warn++;\n else fail++;\n stack.push(...node.children);\n }\n return { pass, warn, fail, totalNodes };\n}\n\n// ── Per-index verification ────────────────────────────────────────────────────\n\nfunction verifyIndex(\n indexName: string,\n contractIndex: { keyPath: IdbIndexIR[\"keyPath\"]; unique: boolean; multiEntry?: boolean },\n actualIndex: IdbIndexIR | undefined,\n storeName: string,\n storePath: string,\n issues: SchemaIssue[]\n): SchemaVerificationNode {\n const indexPath = `${storePath}.indexes.${indexName}`;\n\n if (actualIndex === undefined) {\n const msg = `Index \"${indexName}\" defined in contract is missing from manifest schema`;\n issues.push({\n kind: \"missing_column\",\n table: storeName,\n column: indexName,\n message: msg,\n path: [storeName, indexName],\n });\n return failNode(\"index\", indexName, indexPath, \"missing_column\", msg, contractIndex, undefined);\n }\n\n const children: SchemaVerificationNode[] = [];\n\n // keyPath\n if (!keyPathEquals(contractIndex.keyPath, actualIndex.keyPath)) {\n const msg = `Index \"${indexName}\" keyPath mismatch: expected ${JSON.stringify(contractIndex.keyPath)}, got ${JSON.stringify(actualIndex.keyPath)}`;\n issues.push({\n kind: \"index_mismatch\",\n table: storeName,\n indexOrConstraint: indexName,\n message: msg,\n path: [storeName, indexName],\n });\n children.push(\n failNode(\n \"field\",\n \"keyPath\",\n `${indexPath}.keyPath`,\n \"index_mismatch\",\n msg,\n contractIndex.keyPath,\n actualIndex.keyPath\n )\n );\n } else {\n children.push(passNode(\"field\", \"keyPath\", `${indexPath}.keyPath`));\n }\n\n // unique — treat undefined as false to match how multiEntry is handled\n if ((contractIndex.unique ?? false) !== (actualIndex.unique ?? false)) {\n const msg = `Index \"${indexName}\" unique mismatch: expected ${contractIndex.unique}, got ${actualIndex.unique}`;\n issues.push({\n kind: \"index_mismatch\",\n table: storeName,\n indexOrConstraint: indexName,\n message: msg,\n path: [storeName, indexName],\n });\n children.push(\n failNode(\n \"field\",\n \"unique\",\n `${indexPath}.unique`,\n \"index_mismatch\",\n msg,\n contractIndex.unique,\n actualIndex.unique\n )\n );\n } else {\n children.push(passNode(\"field\", \"unique\", `${indexPath}.unique`));\n }\n\n // multiEntry (only check if contract has it set to true)\n const contractME = contractIndex.multiEntry ?? false;\n const actualME = actualIndex.multiEntry ?? false;\n if (contractME !== actualME) {\n const msg = `Index \"${indexName}\" multiEntry mismatch: expected ${contractME}, got ${actualME}`;\n issues.push({\n kind: \"index_mismatch\",\n table: storeName,\n indexOrConstraint: indexName,\n message: msg,\n path: [storeName, indexName],\n });\n children.push(\n failNode(\"field\", \"multiEntry\", `${indexPath}.multiEntry`, \"index_mismatch\", msg, contractME, actualME)\n );\n } else if (contractME) {\n children.push(passNode(\"field\", \"multiEntry\", `${indexPath}.multiEntry`));\n }\n\n // Extra indexes in manifest not in contract\n // (handled at store level — see verifyStore)\n\n const hasFail = children.some((c) => c.status === \"fail\");\n if (hasFail) {\n return failNode(\n \"index\",\n indexName,\n indexPath,\n \"index_mismatch\",\n `Index \"${indexName}\" has mismatches`,\n null,\n null,\n children\n );\n }\n return passNode(\"index\", indexName, indexPath, children);\n}\n\n// ── Per-store verification ────────────────────────────────────────────────────\n\nfunction verifyStore(\n storeName: string,\n contractStore: {\n keyPath: IdbStoreIR[\"keyPath\"];\n autoIncrement?: boolean;\n indexes?: Record<string, { keyPath: IdbIndexIR[\"keyPath\"]; unique: boolean; multiEntry?: boolean }>;\n },\n actualStore: IdbStoreIR | undefined,\n strict: boolean,\n issues: SchemaIssue[]\n): SchemaVerificationNode {\n const storePath = `storage.stores.${storeName}`;\n\n if (actualStore === undefined) {\n const msg = `Object store \"${storeName}\" defined in contract is missing from manifest schema`;\n issues.push({ kind: \"missing_table\", table: storeName, message: msg, path: [storeName] });\n return failNode(\"collection\", storeName, storePath, \"missing_table\", msg, contractStore, undefined);\n }\n\n const children: SchemaVerificationNode[] = [];\n\n // keyPath\n if (!keyPathEquals(contractStore.keyPath, actualStore.keyPath)) {\n const msg = `Store \"${storeName}\" keyPath mismatch: expected ${JSON.stringify(contractStore.keyPath)}, got ${JSON.stringify(actualStore.keyPath)}`;\n issues.push({ kind: \"primary_key_mismatch\", table: storeName, message: msg, path: [storeName] });\n children.push(\n failNode(\n \"field\",\n \"keyPath\",\n `${storePath}.keyPath`,\n \"primary_key_mismatch\",\n msg,\n contractStore.keyPath,\n actualStore.keyPath\n )\n );\n } else {\n children.push(passNode(\"field\", \"keyPath\", `${storePath}.keyPath`));\n }\n\n // autoIncrement\n const contractAI = contractStore.autoIncrement ?? false;\n const actualAI = actualStore.autoIncrement ?? false;\n if (contractAI !== actualAI) {\n const msg = `Store \"${storeName}\" autoIncrement mismatch: expected ${contractAI}, got ${actualAI}`;\n issues.push({ kind: \"type_mismatch\", table: storeName, message: msg, path: [storeName] });\n children.push(\n failNode(\"field\", \"autoIncrement\", `${storePath}.autoIncrement`, \"type_mismatch\", msg, contractAI, actualAI)\n );\n } else if (contractAI) {\n children.push(passNode(\"field\", \"autoIncrement\", `${storePath}.autoIncrement`));\n }\n\n // indexes\n const contractIndexes = contractStore.indexes ?? {};\n const actualIndexes = actualStore.indexes ?? {};\n\n for (const [indexName, contractIndex] of Object.entries(contractIndexes)) {\n const actualIndex = actualIndexes[indexName];\n children.push(verifyIndex(indexName, contractIndex, actualIndex, storeName, storePath, issues));\n }\n\n // extra indexes in manifest (only relevant in strict mode)\n for (const indexName of Object.keys(actualIndexes)) {\n if (!(indexName in contractIndexes)) {\n const msg = `Index \"${indexName}\" exists in manifest but is not in contract`;\n if (strict) {\n issues.push({\n kind: \"extra_index\",\n table: storeName,\n indexOrConstraint: indexName,\n message: msg,\n path: [storeName, indexName],\n });\n children.push(\n failNode(\n \"index\",\n indexName,\n `${storePath}.indexes.${indexName}`,\n \"extra_index\",\n msg,\n undefined,\n actualIndexes[indexName]\n )\n );\n } else {\n children.push(\n warnNode(\n \"index\",\n indexName,\n `${storePath}.indexes.${indexName}`,\n \"extra_index\",\n msg,\n undefined,\n actualIndexes[indexName]\n )\n );\n }\n }\n }\n\n const hasFail = children.some((c) => c.status === \"fail\");\n if (hasFail) {\n return failNode(\n \"collection\",\n storeName,\n storePath,\n \"schema_mismatch\",\n `Store \"${storeName}\" has schema issues`,\n null,\n null,\n children\n );\n }\n const hasWarn = children.some((c) => c.status === \"warn\");\n if (hasWarn) {\n return {\n status: \"warn\",\n kind: \"collection\",\n name: storeName,\n contractPath: storePath,\n code: \"ok\",\n message: `Store \"${storeName}\" has warnings`,\n expected: null,\n actual: null,\n children,\n };\n }\n return passNode(\"collection\", storeName, storePath, children);\n}\n\n// ── Main entry point ──────────────────────────────────────────────────────────\n\n/**\n * Pure, synchronous schema verification for IndexedDB.\n *\n * Compares the stores and indexes defined in the contract against the manifest\n * schema (which represents the actual IndexedDB database). Returns a\n * {@link VerifyDatabaseSchemaResult} that callers can inspect for issues.\n *\n * Does not perform any I/O — all inputs must already be available.\n *\n * @param contract - Validated IDB contract (what we expect the schema to be).\n * @param schema - Schema IR from the manifest (what the schema actually is).\n * @param strict - When `true`, extra stores/indexes in the manifest are failures.\n * @param options - Optional context paths for metadata / error reporting.\n */\nexport function verifyIdbSchema(\n contract: IdbContract,\n schema: IdbSchemaIR,\n strict: boolean\n): IdbVerifyDatabaseSchemaResult {\n const start = Date.now();\n const issues: SchemaIssue[] = [];\n const storeNodes: SchemaVerificationNode[] = [];\n\n const contractStores = (\n contract.storage as {\n stores: Record<\n string,\n {\n keyPath: IdbStoreIR[\"keyPath\"];\n autoIncrement?: boolean;\n indexes?: Record<string, { keyPath: IdbIndexIR[\"keyPath\"]; unique: boolean; multiEntry?: boolean }>;\n }\n >;\n }\n ).stores;\n const actualStores = schema.stores;\n\n // Verify each store defined in the contract.\n for (const [storeName, contractStore] of Object.entries(contractStores)) {\n const actualStore: IdbStoreIR | undefined = actualStores[storeName];\n storeNodes.push(verifyStore(storeName, contractStore, actualStore, strict, issues));\n }\n\n // Extra stores in manifest (only in strict mode as failures, otherwise warnings).\n for (const [storeName, actualStore] of Object.entries(actualStores)) {\n if (!(storeName in contractStores)) {\n const msg = `Object store \"${storeName}\" exists in manifest but is not in contract`;\n if (strict) {\n issues.push({ kind: \"extra_table\", table: storeName, message: msg, path: [storeName] });\n storeNodes.push(\n failNode(\"collection\", storeName, `storage.stores.${storeName}`, \"extra_table\", msg, undefined, actualStore)\n );\n } else {\n storeNodes.push(\n warnNode(\"collection\", storeName, `storage.stores.${storeName}`, \"extra_table\", msg, undefined, actualStore)\n );\n }\n }\n }\n\n const root: SchemaVerificationNode = {\n status: storeNodes.some((n) => n.status === \"fail\")\n ? \"fail\"\n : storeNodes.some((n) => n.status === \"warn\")\n ? \"warn\"\n : \"pass\",\n kind: \"root\",\n name: \"idb\",\n contractPath: \"storage.stores\",\n code: issues.length === 0 ? \"ok\" : \"schema_mismatch\",\n message: issues.length === 0 ? \"All stores and indexes match\" : `${issues.length} schema issue(s) found`,\n expected: null,\n actual: null,\n children: storeNodes,\n };\n\n // Count statuses including root.\n const allNodes = [root, ...storeNodes];\n const counts = countStatuses(allNodes);\n const ok = root.status !== \"fail\";\n\n const storageHash = (contract.storage as { storageHash: string }).storageHash;\n const profileHash = (contract as { profileHash?: string }).profileHash;\n\n // exactOptionalPropertyTypes: build optional sub-objects conditionally.\n const contractField =\n profileHash !== undefined ? ({ storageHash, profileHash } as const) : ({ storageHash } as const);\n\n const metaField = {\n strict,\n };\n\n const sharedResult = {\n summary: ok ? \"Schema verification passed\" : `Schema verification failed: ${issues.length} issue(s)`,\n contract: contractField,\n target: {\n expected: \"idb\",\n },\n schema: {\n issues,\n root,\n counts,\n },\n meta: metaField,\n timings: { total: Date.now() - start },\n };\n\n if (ok) {\n return { ok: true, ...sharedResult };\n }\n return { ok: false, code: VERIFY_CODE_SCHEMA_FAILURE, ...sharedResult };\n}\n","import type { Contract, ContractMarkerRecord, LedgerEntryRecord } from \"@prisma/orm-framework/contract/types\";\nimport type { TargetBoundComponentDescriptor } from \"@prisma/orm-framework/components/components\";\nimport type {\n ControlDriverInstance,\n ControlFamilyInstance,\n ControlStack,\n SignDatabaseResult,\n VerifyDatabaseResult,\n VerifyDatabaseSchemaResult,\n} from \"@prisma/orm-framework/components/control\";\nimport { VERIFY_CODE_TARGET_MISMATCH } from \"@prisma/orm-framework/components/control\";\nimport type { IdbSchemaIR } from \"./schema-ir\";\nimport { verifyIdbSchema } from \"./schema-verify\";\nimport { validateContract } from \"./validate\";\n\n// ── Structured refusal helpers ────────────────────────────────────────────────\n// IndexedDB only exists in the browser; the CLI runs in Node.js. The control\n// plane's read/write methods therefore return structured failures rather\n// than reading from any kind of file-backed shadow.\n//\n// The refusal code shares a prefix (`IDB-CLI-`) so callers can branch on the\n// family without parsing the summary text.\n\nconst REFUSAL_CODE = \"IDB-CLI-UNSUPPORTED\" as const;\n\n/**\n * Single-string refusal message baked into `summary` because the framework\n * `VerifyDatabaseResult` / `SignDatabaseResult` `meta` field is strict-shaped\n * (`{ contractPath, configPath? }`) and doesn't accept extra explanation\n * fields. The summary is the only free-form text field on every result.\n */\nfunction refusalSummary(action: \"verified\" | \"signed\"): string {\n return (\n `IndexedDB cannot be ${action} from the CLI. ` +\n \"IndexedDB only exists in the browser; the CLI runs in Node.js, so there \" +\n \"is no live database for it to inspect or update. Author migrations with \" +\n \"`prisma-idb migration plan`, validate the chain with \" +\n \"`prisma-idb migration preflight`, and let `createAutoMigratingIdbClient` apply \" +\n \"them the next time the app opens in a browser.\"\n );\n}\n\n// ── exactOptionalPropertyTypes helpers ───────────────────────────────────────\n\nfunction contractInfo(storageHash: string, profileHash: string | undefined) {\n return profileHash !== undefined ? ({ storageHash, profileHash } as const) : ({ storageHash } as const);\n}\n\nfunction verifyMeta(contractPath: string, configPath: string | undefined) {\n return configPath !== undefined ? ({ contractPath, configPath } as const) : ({ contractPath } as const);\n}\n\nfunction signMeta(contractPath: string, configPath: string | undefined) {\n return configPath !== undefined ? ({ contractPath, configPath } as const) : ({ contractPath } as const);\n}\n\n/** Fully-typed IDB control family instance returned by {@link createIdbFamilyInstance}. */\nexport type IdbControlFamilyInstance = ControlFamilyInstance<\"idb\", IdbSchemaIR>;\n\n/**\n * Creates an IDB control family instance for the given control stack.\n *\n * **CLI surface — refusals**: IndexedDB is a browser API, so the CLI cannot\n * read or write the live database. Every method that would normally talk to a\n * database (`verify`, `sign`, `readMarker`, `readAllMarkers`, `introspect`)\n * returns a structured refusal pointing the user at the contract-space\n * authoring + preflight workflow.\n *\n * The CLI-side `db init`, `db update`, and `db verify` commands therefore\n * surface a uniform `IDB-CLI-UNSUPPORTED` envelope rather than silently\n * succeeding.\n *\n * **Active surface**: `deserializeContract` (pure) and `verifySchema` (pure\n * function over an in-memory `IdbSchemaIR`) continue to work — neither\n * needs a live database.\n *\n * @param _stack - The assembled control stack (unused; IDB has no adapter/extension layer).\n */\nexport function createIdbFamilyInstance(_stack: ControlStack<\"idb\", string>): IdbControlFamilyInstance {\n return {\n familyId: \"idb\",\n\n // ── deserializeContract (active, pure) ─────────────────────────────────\n\n deserializeContract(contractJson: unknown): Contract {\n return validateContract(contractJson) as Contract;\n },\n\n // ── verify (CLI refusal) ───────────────────────────────────────────────\n\n async verify(options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n readonly contract: unknown;\n readonly expectedTargetId: string;\n readonly contractPath: string;\n readonly configPath?: string;\n }): Promise<VerifyDatabaseResult> {\n const start = Date.now();\n const contract = validateContract(options.contract);\n const storageHash = (contract.storage as { storageHash: string }).storageHash;\n const profileHash = (contract as { profileHash?: string }).profileHash;\n\n if (options.expectedTargetId !== \"idb\") {\n return {\n ok: false,\n code: VERIFY_CODE_TARGET_MISMATCH,\n summary: `Target mismatch: expected \"idb\", got \"${options.expectedTargetId}\"`,\n contract: contractInfo(storageHash, profileHash),\n target: { expected: options.expectedTargetId, actual: \"idb\" },\n meta: verifyMeta(options.contractPath, options.configPath),\n timings: { total: Date.now() - start },\n };\n }\n\n return {\n ok: false,\n code: REFUSAL_CODE,\n summary: refusalSummary(\"verified\"),\n contract: contractInfo(storageHash, profileHash),\n target: { expected: \"idb\", actual: \"idb\" },\n meta: verifyMeta(options.contractPath, options.configPath),\n timings: { total: Date.now() - start },\n };\n },\n\n // ── verifySchema (active, pure) ────────────────────────────────────────\n\n verifySchema(options: {\n readonly contract: unknown;\n readonly schema: IdbSchemaIR;\n readonly strict: boolean;\n readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<\"idb\", string>>;\n }): VerifyDatabaseSchemaResult {\n const contract = validateContract(options.contract);\n return verifyIdbSchema(contract, options.schema, options.strict);\n },\n\n // ── sign (CLI refusal) ──────────────────────────────────────────────────\n\n async sign(options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n readonly contract: unknown;\n readonly contractPath: string;\n readonly configPath?: string;\n }): Promise<SignDatabaseResult> {\n const start = Date.now();\n const contract = validateContract(options.contract);\n const storageHash = (contract.storage as { storageHash: string }).storageHash;\n const profileHash = (contract as { profileHash?: string }).profileHash ?? \"\";\n\n return {\n ok: false,\n summary: refusalSummary(\"signed\"),\n contract: contractInfo(storageHash, profileHash),\n target: { expected: \"idb\", actual: \"idb\" },\n // `SignDatabaseResult.marker` is required; carry an \"untouched\"\n // record so callers don't crash on null-deref.\n marker: { created: false, updated: false },\n meta: signMeta(options.contractPath, options.configPath),\n timings: { total: Date.now() - start },\n };\n },\n\n // ── readMarker / readAllMarkers (CLI refusal: return null/empty) ───────\n // The framework typing requires `ContractMarkerRecord | null` here, not a\n // structured envelope. Returning `null` is the existing semantics for\n // \"no marker on file\"; we keep that contract so the CLI's downstream\n // logic (e.g. db init's \"create new marker\" path) treats IDB as\n // perpetually empty rather than erroring out of band.\n\n async readMarker(_options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n readonly space: string;\n }): Promise<ContractMarkerRecord | null> {\n return null;\n },\n\n async readAllMarkers(_options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n }): Promise<ReadonlyMap<string, ContractMarkerRecord>> {\n return new Map<string, ContractMarkerRecord>();\n },\n\n async readLedger(_options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n readonly space?: string;\n }): Promise<readonly LedgerEntryRecord[]> {\n return [];\n },\n\n // ── introspect (CLI refusal: return empty schema) ──────────────────────\n // The framework typing requires `IdbSchemaIR` (not a result envelope), so\n // we return an empty schema. The structural refusal is communicated via\n // the sibling `verify`/`sign` methods that the CLI actually surfaces to\n // the user when running `db init` / `db update` / `db verify` against\n // an IDB project.\n\n async introspect(_options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n readonly contract?: unknown;\n }): Promise<IdbSchemaIR> {\n return { stores: {} };\n },\n };\n}\n\nexport type { ControlDriverInstance, SignDatabaseResult, VerifyDatabaseResult, VerifyDatabaseSchemaResult };\nexport type { IdbSchemaIR } from \"./schema-ir\";\n","import type { ControlFamilyDescriptor, ControlStack } from \"@prisma/orm-framework/components/control\";\nimport { idbEmission } from \"./emission\";\nimport { type IdbControlFamilyInstance, createIdbFamilyInstance } from \"./control-instance\";\n\n/**\n * IDB family descriptor — the control-plane entry point for the IDB family.\n *\n * Registered in a Prisma 8 config file under the `family` key:\n * ```ts\n * import idb from '@prisma-idb/family-idb/control';\n * export default defineConfig({ family: idb, ... });\n * ```\n *\n * **Responsibilities:**\n * - Carries the {@link idbEmission} plugin used by `prisma contract emit`\n * to generate `contract.d.ts`.\n * - Acts as a factory: `create(stack)` returns an {@link IdbControlFamilyInstance}\n * with domain-action methods consumed by the CLI.\n */\nexport class IdbFamilyDescriptor implements ControlFamilyDescriptor<\"idb\", IdbControlFamilyInstance> {\n readonly kind = \"family\" as const;\n readonly id = \"idb\";\n readonly familyId = \"idb\" as const;\n readonly version = \"0.0.1\";\n readonly emission = idbEmission;\n\n /**\n * Creates an {@link IdbControlFamilyInstance} for the given control stack.\n *\n * The instance is a plain object whose methods implement the CLI domain\n * actions (validateContract, verify, sign, etc.).\n */\n create(stack: ControlStack<\"idb\", string>): IdbControlFamilyInstance {\n return createIdbFamilyInstance(stack);\n }\n}\n","import { IdbFamilyDescriptor } from \"../core/control-descriptor\";\n\nexport type { IdbControlFamilyInstance } from \"../core/control-instance\";\nexport type { IdbExtensionSpace } from \"../core/extension-space\";\nexport type { IdbSchemaIR, IdbStoreIR, IdbIndexIR } from \"../core/schema-ir\";\nexport type { IdbContract } from \"../core/validate\";\n\nexport default new IdbFamilyDescriptor();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"control.mjs","names":[],"sources":["../../src/core/emission.ts","../../src/core/control-instance.ts","../../src/core/control-descriptor.ts","../../src/exports/control.ts"],"sourcesContent":["import type { Contract, ContractModel } from \"@prisma/orm-framework/contract/types\";\nimport type { EmissionSpi } from \"@prisma/orm-framework/components/emission\";\nimport type { IdbModelStorage, IdbStorage } from \"@prisma-idb/target-idb/pack\";\n\n// ── Serialization utilities ──────────────────────────────────────────────────\n// Adapted from Prisma ORM's domain type generation.\n// These utilities convert runtime JS values into TypeScript type-literal strings\n// that are spliced verbatim into the generated contract.d.ts file.\n\n/**\n * Converts a JavaScript primitive or plain object into a TypeScript type-literal\n * string. Arrays become `readonly [...]` tuples; objects become `{ readonly k: v }`\n * type literals; strings are single-quoted with escaping.\n */\nfunction serializeValue(value: unknown): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"string\") {\n const escaped = value.replace(/\\\\/g, \"\\\\\\\\\").replace(/'/g, \"\\\\'\");\n return `'${escaped}'`;\n }\n if (typeof value === \"number\" || typeof value === \"boolean\") return String(value);\n if (typeof value === \"bigint\") return `${value}n`;\n if (Array.isArray(value)) {\n const items = value.map((v) => serializeValue(v)).join(\", \");\n return `readonly [${items}]`;\n }\n if (typeof value === \"object\") {\n const entries: string[] = [];\n for (const [k, v] of Object.entries(value)) {\n entries.push(`readonly ${serializeObjectKey(k)}: ${serializeValue(v)}`);\n }\n return `{ ${entries.join(\"; \")} }`;\n }\n return \"unknown\";\n}\n\n/**\n * Converts an object key to a safe TypeScript identifier. If the key is already a\n * valid identifier it is returned as-is; otherwise it is wrapped in single-quoted\n * string literal form.\n */\nfunction serializeObjectKey(key: string): string {\n if (/^[$A-Z_a-z][$\\w]*$/.test(key)) return key;\n return serializeValue(key);\n}\n\n// ── EmissionSpi ───────────────────────────────────────────────────────────────\n\n/**\n * IDB family emission plugin.\n *\n * Registered as `emission` on {@link IdbFamilyDescriptor}. The Prisma 8 emitter\n * calls these methods when generating `contract.d.ts` for any schema that targets the\n * IDB family. Each method returns a raw TypeScript source string that the emitter\n * splices into the generated file at the appropriate location.\n *\n * @see `generateContractDts` in `@prisma/orm-toolchain`.\n */\nexport const idbEmission = {\n id: \"idb\",\n\n /**\n * Serializes the full `storage` section of the contract into a TypeScript type\n * literal. The result is used as the first generic argument of `ContractType<...>`\n * in the generated `contract.d.ts`.\n *\n * For IDB the storage shape is:\n * ```ts\n * { readonly stores: { readonly <storeName>: { readonly keyPath: '...'; ... }; ... };\n * readonly storageHash: <storageHashTypeName> }\n * ```\n *\n * @param contract - The resolved Prisma contract.\n * @param storageHashTypeName - Emitter-provided name of the `StorageHash` type alias\n * to embed (e.g. `'StorageHash'`).\n */\n generateStorageType(contract: Contract, storageHashTypeName: string): string {\n const storage = contract.storage as IdbStorage;\n const stores: string[] = [];\n\n for (const [storeName, store] of Object.entries(storage.stores).sort(([a], [b]) => a.localeCompare(b))) {\n const storeParts: string[] = [`readonly keyPath: ${serializeValue(store.keyPath)}`];\n\n if (store.autoIncrement !== undefined) {\n storeParts.push(`readonly autoIncrement: ${store.autoIncrement}`);\n }\n\n const indexes = store.indexes ?? {};\n if (Object.keys(indexes).length > 0) {\n const indexEntries: string[] = [];\n for (const [indexName, index] of Object.entries(indexes).sort(([a], [b]) => a.localeCompare(b))) {\n // Canonicalization drops `unique: false`, so an absent flag means a non-unique index.\n const indexParts = [\n `readonly keyPath: ${serializeValue(index.keyPath)}`,\n `readonly unique: ${index.unique ?? false}`,\n ];\n if (index.multiEntry !== undefined) {\n indexParts.push(`readonly multiEntry: ${index.multiEntry}`);\n }\n indexEntries.push(`readonly ${serializeObjectKey(indexName)}: { ${indexParts.join(\"; \")} }`);\n }\n storeParts.push(`readonly indexes: { ${indexEntries.join(\"; \")} }`);\n } else {\n storeParts.push(`readonly indexes: Record<string, never>`);\n }\n\n stores.push(`readonly ${serializeObjectKey(storeName)}: { ${storeParts.join(\"; \")} }`);\n }\n\n const storesType = stores.length > 0 ? `{ ${stores.join(\"; \")} }` : \"Record<string, never>\";\n\n // v0.12.0: StorageBase now requires `namespaces`. Generate the literal type from\n // the contract's storage namespaces so the emitted storage type satisfies StorageBase.\n const nsEntries: string[] = [];\n for (const [nsId, ns] of Object.entries(\n (storage as unknown as { namespaces?: Record<string, { id: string; entries?: Record<string, unknown> }> })\n .namespaces ?? {}\n ).sort(([a], [b]) => a.localeCompare(b))) {\n const entriesObj = ns.entries ?? {};\n const entriesType = Object.keys(entriesObj).length === 0 ? \"Record<string, never>\" : serializeValue(entriesObj);\n nsEntries.push(\n `readonly ${serializeObjectKey(nsId)}: { readonly id: ${serializeValue(ns.id)}; readonly entries: ${entriesType} }`\n );\n }\n const namespacesType = nsEntries.length > 0 ? `{ ${nsEntries.join(\"; \")} }` : \"Record<string, never>\";\n\n return `{ readonly stores: ${storesType}; readonly namespaces: ${namespacesType}; readonly storageHash: ${storageHashTypeName} }`;\n },\n\n /**\n * Serializes the per-model storage metadata into a TypeScript type literal.\n * The result is used as the `storage` field of the model's entry in the\n * generated `models` type.\n *\n * For IDB, each model maps to exactly one object store:\n * ```ts\n * { readonly storeName: '<name>'; readonly keyPath: '<field>' }\n * ```\n *\n * @param _modelName - The Prisma model name (unused; provided for API symmetry).\n * @param model - The resolved `ContractModel` whose `storage` is cast to\n * {@link IdbModelStorage}.\n */\n generateModelStorageType(_modelName: string, model: ContractModel): string {\n const idbModel = model as ContractModel<IdbModelStorage>;\n return [\n `{ readonly storeName: ${serializeValue(idbModel.storage.storeName)}`,\n `readonly keyPath: ${serializeValue(idbModel.storage.keyPath)} }`,\n ].join(\"; \");\n },\n\n resolveFieldValueSet(_modelName: string, fieldName: string, model: ContractModel, contract: Contract) {\n const ref = model.fields[fieldName]?.valueSet;\n if (ref?.plane !== \"domain\" || ref.entityKind !== \"enum\" || ref.spaceId !== undefined) return undefined;\n const entry = contract.domain.namespaces[ref.namespaceId]?.enum?.[ref.entityName];\n if (entry === undefined) return undefined;\n return {\n codecId: entry.codecId,\n encodedValues: entry.members.map((member) => member.value),\n };\n },\n\n /**\n * Returns the `import type` lines prepended to `contract.d.ts` that bring\n * IDB-family types into scope.\n *\n * Imports {@link IdbContractWithTypeMaps} (the phantom wrapper used by\n * {@link getContractWrapper}) and {@link IdbTypeMaps} (used by\n * {@link getTypeMapsExpression}).\n */\n getFamilyImports(): string[] {\n return [\"import type { IdbContractWithTypeMaps, IdbTypeMaps } from '@prisma-idb/target-idb/pack';\"];\n },\n\n /**\n * Returns additional type alias declarations appended to `contract.d.ts`\n * after the codec and operation type aliases.\n *\n * For IDB this exports `LaneCodecTypes` (used by the generated client to\n * look up codec input/output pairs) as a simple alias of the assembled\n * `CodecTypes`. IDB has no query-operation types, so no further aliases\n * are needed.\n */\n getFamilyTypeAliases(): string {\n return \"export type LaneCodecTypes = CodecTypes;\";\n },\n\n /**\n * Returns the TypeScript expression for the `TypeMaps<...>` generic\n * instantiation written into `contract.d.ts`.\n *\n * IDB has no operation types or query-operation types, so the expression\n * uses the three-argument form of {@link IdbTypeMaps}.\n */\n getTypeMapsExpression(): string {\n return \"IdbTypeMaps<CodecTypes, FieldOutputTypes, FieldInputTypes>\";\n },\n\n /**\n * Returns the final `export type Contract = ...` declaration and convenience\n * re-exports derived from it.\n *\n * @param contractBaseName - Emitter-provided name of the base contract type\n * (always `'ContractBase'`).\n * @param typeMapsName - Emitter-provided name of the assembled TypeMaps type\n * (always `'TypeMaps'`).\n */\n getContractWrapper(contractBaseName: string, typeMapsName: string): string {\n return [\n `export type Contract = IdbContractWithTypeMaps<${contractBaseName}, ${typeMapsName}>;`,\n \"\",\n \"export type Stores = Contract['storage']['stores'];\",\n ].join(\"\\n\");\n },\n} as const satisfies EmissionSpi;\n","import type { Contract, ContractMarkerRecord, LedgerEntryRecord } from \"@prisma/orm-framework/contract/types\";\nimport type { TargetBoundComponentDescriptor } from \"@prisma/orm-framework/components/components\";\nimport type {\n ControlDriverInstance,\n ControlFamilyInstance,\n ControlStack,\n SignDatabaseResult,\n VerifyDatabaseResult,\n VerifyDatabaseSchemaResult,\n} from \"@prisma/orm-framework/components/control\";\nimport { VERIFY_CODE_TARGET_MISMATCH } from \"@prisma/orm-framework/components/control\";\nimport type { IdbSchemaIR } from \"./schema-ir\";\nimport { verifyIdbSchema } from \"./schema-verify\";\nimport { validateContract } from \"./validate\";\n\n// ── Structured refusal helpers ────────────────────────────────────────────────\n// IndexedDB only exists in the browser; the CLI runs in Node.js. The control\n// plane's read/write methods therefore return structured failures rather\n// than reading from any kind of file-backed shadow.\n//\n// The refusal code shares a prefix (`IDB-CLI-`) so callers can branch on the\n// family without parsing the summary text.\n\nconst REFUSAL_CODE = \"IDB-CLI-UNSUPPORTED\" as const;\n\n/**\n * Single-string refusal message baked into `summary` because the framework\n * `VerifyDatabaseResult` / `SignDatabaseResult` `meta` field is strict-shaped\n * (`{ contractPath, configPath? }`) and doesn't accept extra explanation\n * fields. The summary is the only free-form text field on every result.\n */\nfunction refusalSummary(action: \"verified\" | \"signed\"): string {\n return (\n `IndexedDB cannot be ${action} from the CLI. ` +\n \"IndexedDB only exists in the browser; the CLI runs in Node.js, so there \" +\n \"is no live database for it to inspect or update. Author migrations with \" +\n \"`prisma-idb migration plan`, validate the chain with \" +\n \"`prisma-idb migration preflight`, and let `createAutoMigratingIdbClient` apply \" +\n \"them the next time the app opens in a browser.\"\n );\n}\n\n// ── exactOptionalPropertyTypes helpers ───────────────────────────────────────\n\nfunction contractInfo(storageHash: string, profileHash: string | undefined) {\n return profileHash !== undefined ? ({ storageHash, profileHash } as const) : ({ storageHash } as const);\n}\n\nfunction verifyMeta(contractPath: string, configPath: string | undefined) {\n return configPath !== undefined ? ({ contractPath, configPath } as const) : ({ contractPath } as const);\n}\n\nfunction signMeta(contractPath: string, configPath: string | undefined) {\n return configPath !== undefined ? ({ contractPath, configPath } as const) : ({ contractPath } as const);\n}\n\n/** Fully-typed IDB control family instance returned by {@link createIdbFamilyInstance}. */\nexport type IdbControlFamilyInstance = ControlFamilyInstance<\"idb\", IdbSchemaIR>;\n\n/**\n * Creates an IDB control family instance for the given control stack.\n *\n * **CLI surface — refusals**: IndexedDB is a browser API, so the CLI cannot\n * read or write the live database. Every method that would normally talk to a\n * database (`verify`, `sign`, `readMarker`, `readAllMarkers`, `introspect`)\n * returns a structured refusal pointing the user at the contract-space\n * authoring + preflight workflow.\n *\n * The CLI-side `db init`, `db update`, and `db verify` commands therefore\n * surface a uniform `IDB-CLI-UNSUPPORTED` envelope rather than silently\n * succeeding.\n *\n * **Active surface**: `deserializeContract` (pure) and `verifySchema` (pure\n * function over an in-memory `IdbSchemaIR`) continue to work — neither\n * needs a live database.\n *\n * @param _stack - The assembled control stack (unused; IDB has no adapter/extension layer).\n */\nexport function createIdbFamilyInstance(_stack: ControlStack<\"idb\", string>): IdbControlFamilyInstance {\n return {\n familyId: \"idb\",\n\n // ── deserializeContract (active, pure) ─────────────────────────────────\n\n deserializeContract(contractJson: unknown): Contract {\n return validateContract(contractJson) as Contract;\n },\n\n // ── verify (CLI refusal) ───────────────────────────────────────────────\n\n async verify(options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n readonly contract: unknown;\n readonly expectedTargetId: string;\n readonly contractPath: string;\n readonly configPath?: string;\n }): Promise<VerifyDatabaseResult> {\n const start = Date.now();\n const contract = validateContract(options.contract);\n const storageHash = (contract.storage as { storageHash: string }).storageHash;\n const profileHash = (contract as { profileHash?: string }).profileHash;\n\n if (options.expectedTargetId !== \"idb\") {\n return {\n ok: false,\n code: VERIFY_CODE_TARGET_MISMATCH,\n summary: `Target mismatch: expected \"idb\", got \"${options.expectedTargetId}\"`,\n contract: contractInfo(storageHash, profileHash),\n target: { expected: options.expectedTargetId, actual: \"idb\" },\n meta: verifyMeta(options.contractPath, options.configPath),\n timings: { total: Date.now() - start },\n };\n }\n\n return {\n ok: false,\n code: REFUSAL_CODE,\n summary: refusalSummary(\"verified\"),\n contract: contractInfo(storageHash, profileHash),\n target: { expected: \"idb\", actual: \"idb\" },\n meta: verifyMeta(options.contractPath, options.configPath),\n timings: { total: Date.now() - start },\n };\n },\n\n // ── verifySchema (active, pure) ────────────────────────────────────────\n\n verifySchema(options: {\n readonly contract: unknown;\n readonly schema: IdbSchemaIR;\n readonly strict: boolean;\n readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<\"idb\", string>>;\n }): VerifyDatabaseSchemaResult {\n const contract = validateContract(options.contract);\n return verifyIdbSchema(contract, options.schema, options.strict);\n },\n\n // ── sign (CLI refusal) ──────────────────────────────────────────────────\n\n async sign(options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n readonly contract: unknown;\n readonly contractPath: string;\n readonly configPath?: string;\n }): Promise<SignDatabaseResult> {\n const start = Date.now();\n const contract = validateContract(options.contract);\n const storageHash = (contract.storage as { storageHash: string }).storageHash;\n const profileHash = (contract as { profileHash?: string }).profileHash ?? \"\";\n\n return {\n ok: false,\n summary: refusalSummary(\"signed\"),\n contract: contractInfo(storageHash, profileHash),\n target: { expected: \"idb\", actual: \"idb\" },\n // `SignDatabaseResult.marker` is required; carry an \"untouched\"\n // record so callers don't crash on null-deref.\n marker: { created: false, updated: false },\n meta: signMeta(options.contractPath, options.configPath),\n timings: { total: Date.now() - start },\n };\n },\n\n // ── readMarker / readAllMarkers (CLI refusal: return null/empty) ───────\n // The framework typing requires `ContractMarkerRecord | null` here, not a\n // structured envelope. Returning `null` is the existing semantics for\n // \"no marker on file\"; we keep that contract so the CLI's downstream\n // logic (e.g. db init's \"create new marker\" path) treats IDB as\n // perpetually empty rather than erroring out of band.\n\n async readMarker(_options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n readonly space: string;\n }): Promise<ContractMarkerRecord | null> {\n return null;\n },\n\n async readAllMarkers(_options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n }): Promise<ReadonlyMap<string, ContractMarkerRecord>> {\n return new Map<string, ContractMarkerRecord>();\n },\n\n async readLedger(_options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n readonly space?: string;\n }): Promise<readonly LedgerEntryRecord[]> {\n return [];\n },\n\n // ── introspect (CLI refusal: return empty schema) ──────────────────────\n // The framework typing requires `IdbSchemaIR` (not a result envelope), so\n // we return an empty schema. The structural refusal is communicated via\n // the sibling `verify`/`sign` methods that the CLI actually surfaces to\n // the user when running `db init` / `db update` / `db verify` against\n // an IDB project.\n\n async introspect(_options: {\n readonly driver: ControlDriverInstance<\"idb\", string>;\n readonly contract?: unknown;\n }): Promise<IdbSchemaIR> {\n return { stores: {} };\n },\n };\n}\n\nexport type { ControlDriverInstance, SignDatabaseResult, VerifyDatabaseResult, VerifyDatabaseSchemaResult };\nexport type { IdbSchemaIR } from \"./schema-ir\";\n","import type { ControlFamilyDescriptor, ControlStack } from \"@prisma/orm-framework/components/control\";\nimport { idbEmission } from \"./emission\";\nimport { type IdbControlFamilyInstance, createIdbFamilyInstance } from \"./control-instance\";\n\n/**\n * IDB family descriptor — the control-plane entry point for the IDB family.\n *\n * Registered in a Prisma 8 config file under the `family` key:\n * ```ts\n * import idb from '@prisma-idb/family-idb/control';\n * export default defineConfig({ family: idb, ... });\n * ```\n *\n * **Responsibilities:**\n * - Carries the {@link idbEmission} plugin used by `prisma contract emit`\n * to generate `contract.d.ts`.\n * - Acts as a factory: `create(stack)` returns an {@link IdbControlFamilyInstance}\n * with domain-action methods consumed by the CLI.\n */\nexport class IdbFamilyDescriptor implements ControlFamilyDescriptor<\"idb\", IdbControlFamilyInstance> {\n readonly kind = \"family\" as const;\n readonly id = \"idb\";\n readonly familyId = \"idb\" as const;\n readonly version = \"0.0.1\";\n readonly emission = idbEmission;\n\n /**\n * Creates an {@link IdbControlFamilyInstance} for the given control stack.\n *\n * The instance is a plain object whose methods implement the CLI domain\n * actions (validateContract, verify, sign, etc.).\n */\n create(stack: ControlStack<\"idb\", string>): IdbControlFamilyInstance {\n return createIdbFamilyInstance(stack);\n }\n}\n","import { IdbFamilyDescriptor } from \"../core/control-descriptor\";\n\nexport type { IdbControlFamilyInstance } from \"../core/control-instance\";\nexport type { IdbExtensionSpace } from \"../core/extension-space\";\nexport type { IdbSchemaIR, IdbStoreIR, IdbIndexIR } from \"../core/schema-ir\";\nexport type { IdbContract } from \"../core/validate\";\n\nexport default new IdbFamilyDescriptor();\n"],"mappings":"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"}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { t as verifyIdbSchema } from "./schema-verify-B7-xiHem.mjs";
|
|
2
|
+
import { t as validateContract } from "./validate-dwKE5_5B.mjs";
|
|
1
3
|
import { t as chainOrderByMetadata } from "./chain-order-CaHXo5jz.mjs";
|
|
4
|
+
import { computeStorageHash } from "@prisma/orm-framework/contract/hashing";
|
|
2
5
|
import { readFile, readdir } from "node:fs/promises";
|
|
3
6
|
import { join } from "pathe";
|
|
4
7
|
import { applyOneDdlOp, isIdbDdlOp } from "@prisma-idb/target-idb/migration";
|
|
@@ -18,11 +21,10 @@ import { IDBFactory } from "fake-indexeddb";
|
|
|
18
21
|
* a test-only package on a production path. Running it in CI gives the gate
|
|
19
22
|
* without the workflow tax on every author.
|
|
20
23
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* the
|
|
24
|
-
*
|
|
25
|
-
* 8.9 — still deferred to a follow-up).
|
|
24
|
+
* After replay, verifies the actual stores and indexes against the head's
|
|
25
|
+
* contract at `migrations/snapshots/<hash>/contract.json`. The snapshot must
|
|
26
|
+
* carry the head's target hash, and that hash must match its content. Extra
|
|
27
|
+
* stores and indexes fail verification; the internal marker store is excluded.
|
|
26
28
|
*
|
|
27
29
|
* Exit codes: 0 on full chain success; 1 on any failure.
|
|
28
30
|
*/
|
|
@@ -57,7 +59,25 @@ async function runPreflight(opts) {
|
|
|
57
59
|
return 1;
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
|
-
|
|
62
|
+
const head = packages[packages.length - 1];
|
|
63
|
+
const snapshotPath = join(migrationsDir, "snapshots", head.metadata.to, "contract.json");
|
|
64
|
+
try {
|
|
65
|
+
const contract = validateContract(JSON.parse(await readFile(snapshotPath, "utf-8")));
|
|
66
|
+
assertSnapshotIsHead(contract, head.metadata.to);
|
|
67
|
+
const schema = await readSchema(factory, dbName);
|
|
68
|
+
const result = verifyIdbSchema(contract, schema, true);
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
err(`\nSchema drift against head snapshot ${snapshotPath}:\n`);
|
|
71
|
+
for (const issue of result.schema.issues) err(` ${issue.path.join(".")}: ${issue.message}\n`);
|
|
72
|
+
err("\nPreflight failed.\n");
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
} catch (verifyErr) {
|
|
76
|
+
err(`\nCannot verify head snapshot ${snapshotPath}: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}\n`);
|
|
77
|
+
err("Restore the head contract snapshot or regenerate the migration chain.\n\nPreflight failed.\n");
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
out("\nPreflight passed: every migration applies cleanly and the schema matches the head contract.\n");
|
|
61
81
|
return 0;
|
|
62
82
|
}
|
|
63
83
|
async function loadPackages(appDir) {
|
|
@@ -89,6 +109,20 @@ async function loadPackages(appDir) {
|
|
|
89
109
|
}
|
|
90
110
|
return chainOrderByMetadata(unordered);
|
|
91
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Guards against a snapshot copied from another migration: the replayed schema
|
|
114
|
+
* would match it, and verification would pass against the wrong contract.
|
|
115
|
+
*/
|
|
116
|
+
function assertSnapshotIsHead(contract, headHash) {
|
|
117
|
+
const { storageHash, ...storage } = contract.storage;
|
|
118
|
+
if (storageHash !== headHash) throw new Error(`snapshot storageHash ${storageHash} does not match the head migration target ${headHash}`);
|
|
119
|
+
const computedHash = computeStorageHash({
|
|
120
|
+
target: contract.target,
|
|
121
|
+
targetFamily: contract.targetFamily,
|
|
122
|
+
storage
|
|
123
|
+
});
|
|
124
|
+
if (computedHash !== storageHash) throw new Error(`snapshot storageHash ${storageHash} does not match its content (computed ${computedHash})`);
|
|
125
|
+
}
|
|
92
126
|
function applyPackage(input) {
|
|
93
127
|
return new Promise((resolve, reject) => {
|
|
94
128
|
const req = input.factory.open(input.dbName, input.targetVersion);
|
|
@@ -113,7 +147,45 @@ function applyPackage(input) {
|
|
|
113
147
|
req.onerror = () => reject(req.error ?? /* @__PURE__ */ new Error("preflight open request failed"));
|
|
114
148
|
});
|
|
115
149
|
}
|
|
150
|
+
function readSchema(factory, dbName) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const req = factory.open(dbName);
|
|
153
|
+
req.onerror = () => reject(req.error ?? /* @__PURE__ */ new Error("preflight schema open request failed"));
|
|
154
|
+
req.onsuccess = () => {
|
|
155
|
+
const db = req.result;
|
|
156
|
+
try {
|
|
157
|
+
const names = Array.from(db.objectStoreNames).filter((name) => name !== "_prisma_next_marker");
|
|
158
|
+
const stores = {};
|
|
159
|
+
if (names.length > 0) {
|
|
160
|
+
const tx = db.transaction(names, "readonly");
|
|
161
|
+
for (const name of names) {
|
|
162
|
+
const store = tx.objectStore(name);
|
|
163
|
+
const indexes = {};
|
|
164
|
+
for (const indexName of Array.from(store.indexNames)) {
|
|
165
|
+
const index = store.index(indexName);
|
|
166
|
+
indexes[indexName] = {
|
|
167
|
+
keyPath: index.keyPath,
|
|
168
|
+
unique: index.unique,
|
|
169
|
+
multiEntry: index.multiEntry
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
stores[name] = {
|
|
173
|
+
keyPath: store.keyPath ?? [],
|
|
174
|
+
autoIncrement: store.autoIncrement,
|
|
175
|
+
indexes
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
resolve({ stores });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
reject(err);
|
|
182
|
+
} finally {
|
|
183
|
+
db.close();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
}
|
|
116
188
|
//#endregion
|
|
117
189
|
export { runPreflight };
|
|
118
190
|
|
|
119
|
-
//# sourceMappingURL=preflight-
|
|
191
|
+
//# sourceMappingURL=preflight-Bx2q040D.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight-Bx2q040D.mjs","names":[],"sources":["../src/core/preflight.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { IDBFactory } from \"fake-indexeddb\";\nimport type { MigrationMetadata } from \"@prisma/orm-toolchain/migration-tools/metadata\";\nimport { computeMigrationHash } from \"@prisma/orm-toolchain/migration-tools/hash\";\nimport { computeStorageHash } from \"@prisma/orm-framework/contract/hashing\";\nimport { chainOrderByMetadata, type ChainablePackage } from \"./chain-order\";\nimport { applyOneDdlOp, isIdbDdlOp, type IdbDdlOp } from \"@prisma-idb/target-idb/migration\";\nimport { join } from \"pathe\";\nimport { verifyIdbSchema } from \"./schema-verify\";\nimport type { IdbIndexIR, IdbSchemaIR } from \"./schema-ir\";\nimport { validateContract, type IdbContract } from \"./validate\";\n\nexport interface PreflightOptions {\n readonly migrationsDir: string;\n /** Output sinks. Default to `process.stdout`/`process.stderr`. */\n readonly out?: (line: string) => void;\n readonly err?: (line: string) => void;\n}\n\n/**\n * Walk every migration package under `<migrationsDir>/app/` in chain order,\n * applying each package's `ops.json` against a fresh `fake-indexeddb`\n * instance inside an `upgradeneeded` callback. Reports per-step status to\n * stdout and the final outcome via exit code.\n *\n * **Why this exists**: a dedicated preflight command avoids three failure modes\n * of inlining this into `db update`: (a) `fake-indexeddb` disagrees with real\n * browsers on some edge cases and is not a reliable production oracle; (b) it\n * would substitute a runtime check for proper test coverage; (c) it would ship\n * a test-only package on a production path. Running it in CI gives the gate\n * without the workflow tax on every author.\n *\n * After replay, verifies the actual stores and indexes against the head's\n * contract at `migrations/snapshots/<hash>/contract.json`. The snapshot must\n * carry the head's target hash, and that hash must match its content. Extra\n * stores and indexes fail verification; the internal marker store is excluded.\n *\n * Exit codes: 0 on full chain success; 1 on any failure.\n */\nexport async function runPreflight(opts: PreflightOptions): Promise<number> {\n const out = opts.out ?? ((line: string) => process.stdout.write(line));\n const err = opts.err ?? ((line: string) => process.stderr.write(line));\n const migrationsDir = opts.migrationsDir;\n const appDir = join(migrationsDir, \"app\");\n\n const packages = await loadPackages(appDir);\n if (packages.length === 0) {\n out(\"No migration packages found. Nothing to preflight.\\n\");\n return 0;\n }\n\n out(`Preflighting ${packages.length} migration(s) against fake-indexeddb…\\n`);\n\n const factory = new IDBFactory();\n const dbName = \"__preflight__\";\n\n let currentVersion = 0;\n for (const pkg of packages) {\n out(` ${pkg.dirName} … `);\n try {\n currentVersion += 1;\n await applyPackage({ factory, dbName, targetVersion: currentVersion, ops: pkg.ops });\n out(\"ok\\n\");\n } catch (applyErr) {\n out(\"FAILED\\n\");\n err(` ${applyErr instanceof Error ? applyErr.message : String(applyErr)}\\n`);\n err(\"\\nPreflight failed.\\n\");\n return 1;\n }\n }\n\n const head = packages[packages.length - 1]!;\n const snapshotPath = join(migrationsDir, \"snapshots\", head.metadata.to, \"contract.json\");\n try {\n const contract = validateContract(JSON.parse(await readFile(snapshotPath, \"utf-8\")));\n assertSnapshotIsHead(contract, head.metadata.to);\n const schema = await readSchema(factory, dbName);\n const result = verifyIdbSchema(contract, schema, true);\n if (!result.ok) {\n err(`\\nSchema drift against head snapshot ${snapshotPath}:\\n`);\n for (const issue of result.schema.issues) {\n err(` ${issue.path.join(\".\")}: ${issue.message}\\n`);\n }\n err(\"\\nPreflight failed.\\n\");\n return 1;\n }\n } catch (verifyErr) {\n err(\n `\\nCannot verify head snapshot ${snapshotPath}: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}\\n`\n );\n err(\"Restore the head contract snapshot or regenerate the migration chain.\\n\\nPreflight failed.\\n\");\n return 1;\n }\n\n out(\"\\nPreflight passed: every migration applies cleanly and the schema matches the head contract.\\n\");\n return 0;\n}\n\ninterface LoadedPackage extends ChainablePackage {\n readonly metadata: MigrationMetadata;\n readonly ops: readonly IdbDdlOp[];\n}\n\nasync function loadPackages(appDir: string): Promise<LoadedPackage[]> {\n let dirs: string[];\n try {\n dirs = (await readdir(appDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw err;\n }\n\n // Load all packages unordered, then derive chain order by walking\n // metadata.from/to edges. Directory name sort is unreliable when\n // timestamp formats mix between hand-authored baselines and CLI-emitted\n // additions (e.g. T120000 vs T0337).\n const unordered = new Map<string, LoadedPackage>();\n for (const dirName of dirs) {\n const metaRaw = await readFile(join(appDir, dirName, \"migration.json\"), \"utf-8\");\n const opsRaw = await readFile(join(appDir, dirName, \"ops.json\"), \"utf-8\");\n const metadata = JSON.parse(metaRaw) as MigrationMetadata;\n const opsParsed = JSON.parse(opsRaw) as unknown[];\n const ops: IdbDdlOp[] = [];\n for (const op of opsParsed) {\n if (!isIdbDdlOp(op as never)) {\n throw new Error(`Non-IDB op found in ${dirName}/ops.json: ${JSON.stringify(op)}`);\n }\n ops.push(op as IdbDdlOp);\n }\n const computedHash = computeMigrationHash(metadata, ops as unknown as Parameters<typeof computeMigrationHash>[1]);\n if (computedHash !== metadata.migrationHash) {\n throw new Error(\n `Migration hash mismatch in ${dirName}: stored migrationHash ${metadata.migrationHash} ` +\n `does not match computed hash ${computedHash}`\n );\n }\n unordered.set(dirName, { dirName, metadata, ops });\n }\n\n return chainOrderByMetadata(unordered);\n}\n\n/**\n * Guards against a snapshot copied from another migration: the replayed schema\n * would match it, and verification would pass against the wrong contract.\n */\nfunction assertSnapshotIsHead(contract: IdbContract, headHash: string): void {\n const { storageHash, ...storage } = contract.storage;\n if (storageHash !== headHash) {\n throw new Error(`snapshot storageHash ${storageHash} does not match the head migration target ${headHash}`);\n }\n const computedHash = computeStorageHash({ target: contract.target, targetFamily: contract.targetFamily, storage });\n if (computedHash !== storageHash) {\n throw new Error(`snapshot storageHash ${storageHash} does not match its content (computed ${computedHash})`);\n }\n}\n\nfunction applyPackage(input: {\n readonly factory: IDBFactory;\n readonly dbName: string;\n readonly targetVersion: number;\n readonly ops: readonly IdbDdlOp[];\n}): Promise<void> {\n return new Promise((resolve, reject) => {\n const req = input.factory.open(input.dbName, input.targetVersion);\n req.onupgradeneeded = (event) => {\n const target = event.target as IDBOpenDBRequest;\n const db = target.result;\n const tx = target.transaction;\n if (tx === null) {\n reject(new Error(\"upgradeneeded fired with null version-change transaction\"));\n return;\n }\n try {\n for (const op of input.ops) {\n applyOneDdlOp(db, tx, op);\n }\n } catch (err) {\n reject(err);\n }\n };\n req.onsuccess = () => {\n req.result.close();\n resolve();\n };\n req.onerror = () => reject(req.error ?? new Error(\"preflight open request failed\"));\n });\n}\n\nfunction readSchema(factory: IDBFactory, dbName: string): Promise<IdbSchemaIR> {\n return new Promise((resolve, reject) => {\n const req = factory.open(dbName);\n req.onerror = () => reject(req.error ?? new Error(\"preflight schema open request failed\"));\n req.onsuccess = () => {\n const db = req.result;\n try {\n // The marker belongs to the runtime, not the app contract's storage schema.\n const names = Array.from(db.objectStoreNames).filter((name) => name !== \"_prisma_next_marker\");\n const stores: IdbSchemaIR[\"stores\"] = {};\n if (names.length > 0) {\n const tx = db.transaction(names, \"readonly\");\n for (const name of names) {\n const store = tx.objectStore(name);\n const indexes: Record<string, IdbIndexIR> = {};\n for (const indexName of Array.from(store.indexNames)) {\n const index = store.index(indexName);\n indexes[indexName] = { keyPath: index.keyPath, unique: index.unique, multiEntry: index.multiEntry };\n }\n stores[name] = { keyPath: store.keyPath ?? [], autoIncrement: store.autoIncrement, indexes };\n }\n }\n resolve({ stores });\n } catch (err) {\n reject(err);\n } finally {\n db.close();\n }\n };\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { VERIFY_CODE_SCHEMA_FAILURE } from "@prisma/orm-framework/components/control";
|
|
2
|
+
import { keyPathEquals } from "@prisma-idb/target-idb/pack";
|
|
3
|
+
//#region src/core/schema-verify.ts
|
|
4
|
+
function passNode(kind, name, contractPath, children = []) {
|
|
5
|
+
return {
|
|
6
|
+
status: "pass",
|
|
7
|
+
kind,
|
|
8
|
+
name,
|
|
9
|
+
contractPath,
|
|
10
|
+
code: "ok",
|
|
11
|
+
message: `${name} matches`,
|
|
12
|
+
expected: null,
|
|
13
|
+
actual: null,
|
|
14
|
+
children
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function failNode(kind, name, contractPath, code, message, expected, actual, children = []) {
|
|
18
|
+
return {
|
|
19
|
+
status: "fail",
|
|
20
|
+
kind,
|
|
21
|
+
name,
|
|
22
|
+
contractPath,
|
|
23
|
+
code,
|
|
24
|
+
message,
|
|
25
|
+
expected,
|
|
26
|
+
actual,
|
|
27
|
+
children
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function warnNode(kind, name, contractPath, code, message, expected, actual, children = []) {
|
|
31
|
+
return {
|
|
32
|
+
status: "warn",
|
|
33
|
+
kind,
|
|
34
|
+
name,
|
|
35
|
+
contractPath,
|
|
36
|
+
code,
|
|
37
|
+
message,
|
|
38
|
+
expected,
|
|
39
|
+
actual,
|
|
40
|
+
children
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function countStatuses(nodes) {
|
|
44
|
+
let pass = 0, warn = 0, fail = 0, totalNodes = 0;
|
|
45
|
+
const stack = [...nodes];
|
|
46
|
+
while (stack.length > 0) {
|
|
47
|
+
const node = stack.pop();
|
|
48
|
+
totalNodes++;
|
|
49
|
+
if (node.status === "pass") pass++;
|
|
50
|
+
else if (node.status === "warn") warn++;
|
|
51
|
+
else fail++;
|
|
52
|
+
stack.push(...node.children);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
pass,
|
|
56
|
+
warn,
|
|
57
|
+
fail,
|
|
58
|
+
totalNodes
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function verifyIndex(indexName, contractIndex, actualIndex, storeName, storePath, issues) {
|
|
62
|
+
const indexPath = `${storePath}.indexes.${indexName}`;
|
|
63
|
+
if (actualIndex === void 0) {
|
|
64
|
+
const msg = `Index "${indexName}" defined in contract is missing from manifest schema`;
|
|
65
|
+
issues.push({
|
|
66
|
+
kind: "missing_column",
|
|
67
|
+
table: storeName,
|
|
68
|
+
column: indexName,
|
|
69
|
+
message: msg,
|
|
70
|
+
path: [storeName, indexName]
|
|
71
|
+
});
|
|
72
|
+
return failNode("index", indexName, indexPath, "missing_column", msg, contractIndex, void 0);
|
|
73
|
+
}
|
|
74
|
+
const children = [];
|
|
75
|
+
if (!keyPathEquals(contractIndex.keyPath, actualIndex.keyPath)) {
|
|
76
|
+
const msg = `Index "${indexName}" keyPath mismatch: expected ${JSON.stringify(contractIndex.keyPath)}, got ${JSON.stringify(actualIndex.keyPath)}`;
|
|
77
|
+
issues.push({
|
|
78
|
+
kind: "index_mismatch",
|
|
79
|
+
table: storeName,
|
|
80
|
+
indexOrConstraint: indexName,
|
|
81
|
+
message: msg,
|
|
82
|
+
path: [storeName, indexName]
|
|
83
|
+
});
|
|
84
|
+
children.push(failNode("field", "keyPath", `${indexPath}.keyPath`, "index_mismatch", msg, contractIndex.keyPath, actualIndex.keyPath));
|
|
85
|
+
} else children.push(passNode("field", "keyPath", `${indexPath}.keyPath`));
|
|
86
|
+
if ((contractIndex.unique ?? false) !== (actualIndex.unique ?? false)) {
|
|
87
|
+
const msg = `Index "${indexName}" unique mismatch: expected ${contractIndex.unique}, got ${actualIndex.unique}`;
|
|
88
|
+
issues.push({
|
|
89
|
+
kind: "index_mismatch",
|
|
90
|
+
table: storeName,
|
|
91
|
+
indexOrConstraint: indexName,
|
|
92
|
+
message: msg,
|
|
93
|
+
path: [storeName, indexName]
|
|
94
|
+
});
|
|
95
|
+
children.push(failNode("field", "unique", `${indexPath}.unique`, "index_mismatch", msg, contractIndex.unique, actualIndex.unique));
|
|
96
|
+
} else children.push(passNode("field", "unique", `${indexPath}.unique`));
|
|
97
|
+
const contractME = contractIndex.multiEntry ?? false;
|
|
98
|
+
const actualME = actualIndex.multiEntry ?? false;
|
|
99
|
+
if (contractME !== actualME) {
|
|
100
|
+
const msg = `Index "${indexName}" multiEntry mismatch: expected ${contractME}, got ${actualME}`;
|
|
101
|
+
issues.push({
|
|
102
|
+
kind: "index_mismatch",
|
|
103
|
+
table: storeName,
|
|
104
|
+
indexOrConstraint: indexName,
|
|
105
|
+
message: msg,
|
|
106
|
+
path: [storeName, indexName]
|
|
107
|
+
});
|
|
108
|
+
children.push(failNode("field", "multiEntry", `${indexPath}.multiEntry`, "index_mismatch", msg, contractME, actualME));
|
|
109
|
+
} else if (contractME) children.push(passNode("field", "multiEntry", `${indexPath}.multiEntry`));
|
|
110
|
+
if (children.some((c) => c.status === "fail")) return failNode("index", indexName, indexPath, "index_mismatch", `Index "${indexName}" has mismatches`, null, null, children);
|
|
111
|
+
return passNode("index", indexName, indexPath, children);
|
|
112
|
+
}
|
|
113
|
+
function verifyStore(storeName, contractStore, actualStore, strict, issues) {
|
|
114
|
+
const storePath = `storage.stores.${storeName}`;
|
|
115
|
+
if (actualStore === void 0) {
|
|
116
|
+
const msg = `Object store "${storeName}" defined in contract is missing from manifest schema`;
|
|
117
|
+
issues.push({
|
|
118
|
+
kind: "missing_table",
|
|
119
|
+
table: storeName,
|
|
120
|
+
message: msg,
|
|
121
|
+
path: [storeName]
|
|
122
|
+
});
|
|
123
|
+
return failNode("collection", storeName, storePath, "missing_table", msg, contractStore, void 0);
|
|
124
|
+
}
|
|
125
|
+
const children = [];
|
|
126
|
+
if (!keyPathEquals(contractStore.keyPath, actualStore.keyPath)) {
|
|
127
|
+
const msg = `Store "${storeName}" keyPath mismatch: expected ${JSON.stringify(contractStore.keyPath)}, got ${JSON.stringify(actualStore.keyPath)}`;
|
|
128
|
+
issues.push({
|
|
129
|
+
kind: "primary_key_mismatch",
|
|
130
|
+
table: storeName,
|
|
131
|
+
message: msg,
|
|
132
|
+
path: [storeName]
|
|
133
|
+
});
|
|
134
|
+
children.push(failNode("field", "keyPath", `${storePath}.keyPath`, "primary_key_mismatch", msg, contractStore.keyPath, actualStore.keyPath));
|
|
135
|
+
} else children.push(passNode("field", "keyPath", `${storePath}.keyPath`));
|
|
136
|
+
const contractAI = contractStore.autoIncrement ?? false;
|
|
137
|
+
const actualAI = actualStore.autoIncrement ?? false;
|
|
138
|
+
if (contractAI !== actualAI) {
|
|
139
|
+
const msg = `Store "${storeName}" autoIncrement mismatch: expected ${contractAI}, got ${actualAI}`;
|
|
140
|
+
issues.push({
|
|
141
|
+
kind: "type_mismatch",
|
|
142
|
+
table: storeName,
|
|
143
|
+
message: msg,
|
|
144
|
+
path: [storeName]
|
|
145
|
+
});
|
|
146
|
+
children.push(failNode("field", "autoIncrement", `${storePath}.autoIncrement`, "type_mismatch", msg, contractAI, actualAI));
|
|
147
|
+
} else if (contractAI) children.push(passNode("field", "autoIncrement", `${storePath}.autoIncrement`));
|
|
148
|
+
const contractIndexes = contractStore.indexes ?? {};
|
|
149
|
+
const actualIndexes = actualStore.indexes ?? {};
|
|
150
|
+
for (const [indexName, contractIndex] of Object.entries(contractIndexes)) {
|
|
151
|
+
const actualIndex = actualIndexes[indexName];
|
|
152
|
+
children.push(verifyIndex(indexName, contractIndex, actualIndex, storeName, storePath, issues));
|
|
153
|
+
}
|
|
154
|
+
for (const indexName of Object.keys(actualIndexes)) if (!(indexName in contractIndexes)) {
|
|
155
|
+
const msg = `Index "${indexName}" exists in manifest but is not in contract`;
|
|
156
|
+
if (strict) {
|
|
157
|
+
issues.push({
|
|
158
|
+
kind: "extra_index",
|
|
159
|
+
table: storeName,
|
|
160
|
+
indexOrConstraint: indexName,
|
|
161
|
+
message: msg,
|
|
162
|
+
path: [storeName, indexName]
|
|
163
|
+
});
|
|
164
|
+
children.push(failNode("index", indexName, `${storePath}.indexes.${indexName}`, "extra_index", msg, void 0, actualIndexes[indexName]));
|
|
165
|
+
} else children.push(warnNode("index", indexName, `${storePath}.indexes.${indexName}`, "extra_index", msg, void 0, actualIndexes[indexName]));
|
|
166
|
+
}
|
|
167
|
+
if (children.some((c) => c.status === "fail")) return failNode("collection", storeName, storePath, "schema_mismatch", `Store "${storeName}" has schema issues`, null, null, children);
|
|
168
|
+
if (children.some((c) => c.status === "warn")) return {
|
|
169
|
+
status: "warn",
|
|
170
|
+
kind: "collection",
|
|
171
|
+
name: storeName,
|
|
172
|
+
contractPath: storePath,
|
|
173
|
+
code: "ok",
|
|
174
|
+
message: `Store "${storeName}" has warnings`,
|
|
175
|
+
expected: null,
|
|
176
|
+
actual: null,
|
|
177
|
+
children
|
|
178
|
+
};
|
|
179
|
+
return passNode("collection", storeName, storePath, children);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Pure, synchronous schema verification for IndexedDB.
|
|
183
|
+
*
|
|
184
|
+
* Compares the stores and indexes defined in the contract against the manifest
|
|
185
|
+
* schema (which represents the actual IndexedDB database). Returns a
|
|
186
|
+
* {@link VerifyDatabaseSchemaResult} that callers can inspect for issues.
|
|
187
|
+
*
|
|
188
|
+
* Does not perform any I/O — all inputs must already be available.
|
|
189
|
+
*
|
|
190
|
+
* @param contract - Validated IDB contract (what we expect the schema to be).
|
|
191
|
+
* @param schema - Schema IR from the manifest (what the schema actually is).
|
|
192
|
+
* @param strict - When `true`, extra stores/indexes in the manifest are failures.
|
|
193
|
+
* @param options - Optional context paths for metadata / error reporting.
|
|
194
|
+
*/
|
|
195
|
+
function verifyIdbSchema(contract, schema, strict) {
|
|
196
|
+
const start = Date.now();
|
|
197
|
+
const issues = [];
|
|
198
|
+
const storeNodes = [];
|
|
199
|
+
const contractStores = contract.storage.stores;
|
|
200
|
+
const actualStores = schema.stores;
|
|
201
|
+
for (const [storeName, contractStore] of Object.entries(contractStores)) {
|
|
202
|
+
const actualStore = actualStores[storeName];
|
|
203
|
+
storeNodes.push(verifyStore(storeName, contractStore, actualStore, strict, issues));
|
|
204
|
+
}
|
|
205
|
+
for (const [storeName, actualStore] of Object.entries(actualStores)) if (!(storeName in contractStores)) {
|
|
206
|
+
const msg = `Object store "${storeName}" exists in manifest but is not in contract`;
|
|
207
|
+
if (strict) {
|
|
208
|
+
issues.push({
|
|
209
|
+
kind: "extra_table",
|
|
210
|
+
table: storeName,
|
|
211
|
+
message: msg,
|
|
212
|
+
path: [storeName]
|
|
213
|
+
});
|
|
214
|
+
storeNodes.push(failNode("collection", storeName, `storage.stores.${storeName}`, "extra_table", msg, void 0, actualStore));
|
|
215
|
+
} else storeNodes.push(warnNode("collection", storeName, `storage.stores.${storeName}`, "extra_table", msg, void 0, actualStore));
|
|
216
|
+
}
|
|
217
|
+
const root = {
|
|
218
|
+
status: storeNodes.some((n) => n.status === "fail") ? "fail" : storeNodes.some((n) => n.status === "warn") ? "warn" : "pass",
|
|
219
|
+
kind: "root",
|
|
220
|
+
name: "idb",
|
|
221
|
+
contractPath: "storage.stores",
|
|
222
|
+
code: issues.length === 0 ? "ok" : "schema_mismatch",
|
|
223
|
+
message: issues.length === 0 ? "All stores and indexes match" : `${issues.length} schema issue(s) found`,
|
|
224
|
+
expected: null,
|
|
225
|
+
actual: null,
|
|
226
|
+
children: storeNodes
|
|
227
|
+
};
|
|
228
|
+
const counts = countStatuses([root, ...storeNodes]);
|
|
229
|
+
const ok = root.status !== "fail";
|
|
230
|
+
const storageHash = contract.storage.storageHash;
|
|
231
|
+
const profileHash = contract.profileHash;
|
|
232
|
+
const contractField = profileHash !== void 0 ? {
|
|
233
|
+
storageHash,
|
|
234
|
+
profileHash
|
|
235
|
+
} : { storageHash };
|
|
236
|
+
const metaField = { strict };
|
|
237
|
+
const sharedResult = {
|
|
238
|
+
summary: ok ? "Schema verification passed" : `Schema verification failed: ${issues.length} issue(s)`,
|
|
239
|
+
contract: contractField,
|
|
240
|
+
target: { expected: "idb" },
|
|
241
|
+
schema: {
|
|
242
|
+
issues,
|
|
243
|
+
root,
|
|
244
|
+
counts
|
|
245
|
+
},
|
|
246
|
+
meta: metaField,
|
|
247
|
+
timings: { total: Date.now() - start }
|
|
248
|
+
};
|
|
249
|
+
if (ok) return {
|
|
250
|
+
ok: true,
|
|
251
|
+
...sharedResult
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
code: VERIFY_CODE_SCHEMA_FAILURE,
|
|
256
|
+
...sharedResult
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
export { verifyIdbSchema as t };
|
|
261
|
+
|
|
262
|
+
//# sourceMappingURL=schema-verify-B7-xiHem.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-verify-B7-xiHem.mjs","names":[],"sources":["../src/core/schema-verify.ts"],"sourcesContent":["import {\n VERIFY_CODE_SCHEMA_FAILURE,\n type SchemaDiffIssue,\n type VerifyDatabaseSchemaResult,\n} from \"@prisma/orm-framework/components/control\";\nimport { keyPathEquals } from \"@prisma-idb/target-idb/pack\";\nimport type { IdbContract } from \"./validate\";\nimport type { IdbIndexIR, IdbSchemaIR, IdbStoreIR } from \"./schema-ir\";\n\n/**\n * IDB's own presentation tree for `db verify` output. The framework's\n * `VerifyDatabaseSchemaResult` (0.16.0, TML-3028/#921/#992) dropped the\n * shared verification-tree vocabulary (`SchemaVerificationNode`) in favor of\n * a flat `SchemaDiffIssue[]` produced by a generic `DiffableNode` tree\n * differ. IDB's schema is only two levels deep (store → index), so adopting\n * that generic differ isn't worth the ceremony — this keeps the richer\n * pass/warn/fail tree as an IDB-local addition on top of the (still\n * satisfied) `schema.issues` contract.\n */\ninterface SchemaVerificationNode {\n readonly status: \"pass\" | \"warn\" | \"fail\";\n readonly kind: string;\n readonly name: string;\n readonly contractPath: string;\n readonly code: string;\n readonly message: string;\n readonly expected: unknown;\n readonly actual: unknown;\n readonly children: readonly SchemaVerificationNode[];\n}\n\n/** IDB's issue shape, extended with `path` so it satisfies `SchemaDiffIssue`. */\ntype SchemaIssue = SchemaDiffIssue & {\n readonly kind: string;\n readonly table: string;\n readonly column?: string;\n readonly indexOrConstraint?: string;\n readonly message: string;\n};\n\n/**\n * `verifyIdbSchema`'s actual return shape: a strict superset of\n * `VerifyDatabaseSchemaResult` that keeps the pass/warn/fail tree and node\n * counts as IDB-local additions. Assignable anywhere a plain\n * `VerifyDatabaseSchemaResult` is expected (see `control-instance.ts`).\n */\nexport interface IdbVerifyDatabaseSchemaResult extends Omit<VerifyDatabaseSchemaResult, \"schema\"> {\n readonly schema: {\n readonly issues: readonly SchemaIssue[];\n readonly warnings?: { readonly issues: readonly SchemaIssue[] };\n readonly root: SchemaVerificationNode;\n readonly counts: {\n readonly pass: number;\n readonly warn: number;\n readonly fail: number;\n readonly totalNodes: number;\n };\n };\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction passNode(\n kind: string,\n name: string,\n contractPath: string,\n children: readonly SchemaVerificationNode[] = []\n): SchemaVerificationNode {\n return {\n status: \"pass\",\n kind,\n name,\n contractPath,\n code: \"ok\",\n message: `${name} matches`,\n expected: null,\n actual: null,\n children,\n };\n}\n\nfunction failNode(\n kind: string,\n name: string,\n contractPath: string,\n code: string,\n message: string,\n expected: unknown,\n actual: unknown,\n children: readonly SchemaVerificationNode[] = []\n): SchemaVerificationNode {\n return {\n status: \"fail\",\n kind,\n name,\n contractPath,\n code,\n message,\n expected,\n actual,\n children,\n };\n}\n\nfunction warnNode(\n kind: string,\n name: string,\n contractPath: string,\n code: string,\n message: string,\n expected: unknown,\n actual: unknown,\n children: readonly SchemaVerificationNode[] = []\n): SchemaVerificationNode {\n return {\n status: \"warn\",\n kind,\n name,\n contractPath,\n code,\n message,\n expected,\n actual,\n children,\n };\n}\n\nfunction countStatuses(nodes: readonly SchemaVerificationNode[]): {\n pass: number;\n warn: number;\n fail: number;\n totalNodes: number;\n} {\n let pass = 0,\n warn = 0,\n fail = 0,\n totalNodes = 0;\n const stack = [...nodes];\n while (stack.length > 0) {\n const node = stack.pop()!;\n totalNodes++;\n if (node.status === \"pass\") pass++;\n else if (node.status === \"warn\") warn++;\n else fail++;\n stack.push(...node.children);\n }\n return { pass, warn, fail, totalNodes };\n}\n\n// ── Per-index verification ────────────────────────────────────────────────────\n\nfunction verifyIndex(\n indexName: string,\n contractIndex: { keyPath: IdbIndexIR[\"keyPath\"]; unique: boolean; multiEntry?: boolean },\n actualIndex: IdbIndexIR | undefined,\n storeName: string,\n storePath: string,\n issues: SchemaIssue[]\n): SchemaVerificationNode {\n const indexPath = `${storePath}.indexes.${indexName}`;\n\n if (actualIndex === undefined) {\n const msg = `Index \"${indexName}\" defined in contract is missing from manifest schema`;\n issues.push({\n kind: \"missing_column\",\n table: storeName,\n column: indexName,\n message: msg,\n path: [storeName, indexName],\n });\n return failNode(\"index\", indexName, indexPath, \"missing_column\", msg, contractIndex, undefined);\n }\n\n const children: SchemaVerificationNode[] = [];\n\n // keyPath\n if (!keyPathEquals(contractIndex.keyPath, actualIndex.keyPath)) {\n const msg = `Index \"${indexName}\" keyPath mismatch: expected ${JSON.stringify(contractIndex.keyPath)}, got ${JSON.stringify(actualIndex.keyPath)}`;\n issues.push({\n kind: \"index_mismatch\",\n table: storeName,\n indexOrConstraint: indexName,\n message: msg,\n path: [storeName, indexName],\n });\n children.push(\n failNode(\n \"field\",\n \"keyPath\",\n `${indexPath}.keyPath`,\n \"index_mismatch\",\n msg,\n contractIndex.keyPath,\n actualIndex.keyPath\n )\n );\n } else {\n children.push(passNode(\"field\", \"keyPath\", `${indexPath}.keyPath`));\n }\n\n // unique — treat undefined as false to match how multiEntry is handled\n if ((contractIndex.unique ?? false) !== (actualIndex.unique ?? false)) {\n const msg = `Index \"${indexName}\" unique mismatch: expected ${contractIndex.unique}, got ${actualIndex.unique}`;\n issues.push({\n kind: \"index_mismatch\",\n table: storeName,\n indexOrConstraint: indexName,\n message: msg,\n path: [storeName, indexName],\n });\n children.push(\n failNode(\n \"field\",\n \"unique\",\n `${indexPath}.unique`,\n \"index_mismatch\",\n msg,\n contractIndex.unique,\n actualIndex.unique\n )\n );\n } else {\n children.push(passNode(\"field\", \"unique\", `${indexPath}.unique`));\n }\n\n // multiEntry (only check if contract has it set to true)\n const contractME = contractIndex.multiEntry ?? false;\n const actualME = actualIndex.multiEntry ?? false;\n if (contractME !== actualME) {\n const msg = `Index \"${indexName}\" multiEntry mismatch: expected ${contractME}, got ${actualME}`;\n issues.push({\n kind: \"index_mismatch\",\n table: storeName,\n indexOrConstraint: indexName,\n message: msg,\n path: [storeName, indexName],\n });\n children.push(\n failNode(\"field\", \"multiEntry\", `${indexPath}.multiEntry`, \"index_mismatch\", msg, contractME, actualME)\n );\n } else if (contractME) {\n children.push(passNode(\"field\", \"multiEntry\", `${indexPath}.multiEntry`));\n }\n\n // Extra indexes in manifest not in contract\n // (handled at store level — see verifyStore)\n\n const hasFail = children.some((c) => c.status === \"fail\");\n if (hasFail) {\n return failNode(\n \"index\",\n indexName,\n indexPath,\n \"index_mismatch\",\n `Index \"${indexName}\" has mismatches`,\n null,\n null,\n children\n );\n }\n return passNode(\"index\", indexName, indexPath, children);\n}\n\n// ── Per-store verification ────────────────────────────────────────────────────\n\nfunction verifyStore(\n storeName: string,\n contractStore: {\n keyPath: IdbStoreIR[\"keyPath\"];\n autoIncrement?: boolean;\n indexes?: Record<string, { keyPath: IdbIndexIR[\"keyPath\"]; unique: boolean; multiEntry?: boolean }>;\n },\n actualStore: IdbStoreIR | undefined,\n strict: boolean,\n issues: SchemaIssue[]\n): SchemaVerificationNode {\n const storePath = `storage.stores.${storeName}`;\n\n if (actualStore === undefined) {\n const msg = `Object store \"${storeName}\" defined in contract is missing from manifest schema`;\n issues.push({ kind: \"missing_table\", table: storeName, message: msg, path: [storeName] });\n return failNode(\"collection\", storeName, storePath, \"missing_table\", msg, contractStore, undefined);\n }\n\n const children: SchemaVerificationNode[] = [];\n\n // keyPath\n if (!keyPathEquals(contractStore.keyPath, actualStore.keyPath)) {\n const msg = `Store \"${storeName}\" keyPath mismatch: expected ${JSON.stringify(contractStore.keyPath)}, got ${JSON.stringify(actualStore.keyPath)}`;\n issues.push({ kind: \"primary_key_mismatch\", table: storeName, message: msg, path: [storeName] });\n children.push(\n failNode(\n \"field\",\n \"keyPath\",\n `${storePath}.keyPath`,\n \"primary_key_mismatch\",\n msg,\n contractStore.keyPath,\n actualStore.keyPath\n )\n );\n } else {\n children.push(passNode(\"field\", \"keyPath\", `${storePath}.keyPath`));\n }\n\n // autoIncrement\n const contractAI = contractStore.autoIncrement ?? false;\n const actualAI = actualStore.autoIncrement ?? false;\n if (contractAI !== actualAI) {\n const msg = `Store \"${storeName}\" autoIncrement mismatch: expected ${contractAI}, got ${actualAI}`;\n issues.push({ kind: \"type_mismatch\", table: storeName, message: msg, path: [storeName] });\n children.push(\n failNode(\"field\", \"autoIncrement\", `${storePath}.autoIncrement`, \"type_mismatch\", msg, contractAI, actualAI)\n );\n } else if (contractAI) {\n children.push(passNode(\"field\", \"autoIncrement\", `${storePath}.autoIncrement`));\n }\n\n // indexes\n const contractIndexes = contractStore.indexes ?? {};\n const actualIndexes = actualStore.indexes ?? {};\n\n for (const [indexName, contractIndex] of Object.entries(contractIndexes)) {\n const actualIndex = actualIndexes[indexName];\n children.push(verifyIndex(indexName, contractIndex, actualIndex, storeName, storePath, issues));\n }\n\n // extra indexes in manifest (only relevant in strict mode)\n for (const indexName of Object.keys(actualIndexes)) {\n if (!(indexName in contractIndexes)) {\n const msg = `Index \"${indexName}\" exists in manifest but is not in contract`;\n if (strict) {\n issues.push({\n kind: \"extra_index\",\n table: storeName,\n indexOrConstraint: indexName,\n message: msg,\n path: [storeName, indexName],\n });\n children.push(\n failNode(\n \"index\",\n indexName,\n `${storePath}.indexes.${indexName}`,\n \"extra_index\",\n msg,\n undefined,\n actualIndexes[indexName]\n )\n );\n } else {\n children.push(\n warnNode(\n \"index\",\n indexName,\n `${storePath}.indexes.${indexName}`,\n \"extra_index\",\n msg,\n undefined,\n actualIndexes[indexName]\n )\n );\n }\n }\n }\n\n const hasFail = children.some((c) => c.status === \"fail\");\n if (hasFail) {\n return failNode(\n \"collection\",\n storeName,\n storePath,\n \"schema_mismatch\",\n `Store \"${storeName}\" has schema issues`,\n null,\n null,\n children\n );\n }\n const hasWarn = children.some((c) => c.status === \"warn\");\n if (hasWarn) {\n return {\n status: \"warn\",\n kind: \"collection\",\n name: storeName,\n contractPath: storePath,\n code: \"ok\",\n message: `Store \"${storeName}\" has warnings`,\n expected: null,\n actual: null,\n children,\n };\n }\n return passNode(\"collection\", storeName, storePath, children);\n}\n\n// ── Main entry point ──────────────────────────────────────────────────────────\n\n/**\n * Pure, synchronous schema verification for IndexedDB.\n *\n * Compares the stores and indexes defined in the contract against the manifest\n * schema (which represents the actual IndexedDB database). Returns a\n * {@link VerifyDatabaseSchemaResult} that callers can inspect for issues.\n *\n * Does not perform any I/O — all inputs must already be available.\n *\n * @param contract - Validated IDB contract (what we expect the schema to be).\n * @param schema - Schema IR from the manifest (what the schema actually is).\n * @param strict - When `true`, extra stores/indexes in the manifest are failures.\n * @param options - Optional context paths for metadata / error reporting.\n */\nexport function verifyIdbSchema(\n contract: IdbContract,\n schema: IdbSchemaIR,\n strict: boolean\n): IdbVerifyDatabaseSchemaResult {\n const start = Date.now();\n const issues: SchemaIssue[] = [];\n const storeNodes: SchemaVerificationNode[] = [];\n\n const contractStores = (\n contract.storage as {\n stores: Record<\n string,\n {\n keyPath: IdbStoreIR[\"keyPath\"];\n autoIncrement?: boolean;\n indexes?: Record<string, { keyPath: IdbIndexIR[\"keyPath\"]; unique: boolean; multiEntry?: boolean }>;\n }\n >;\n }\n ).stores;\n const actualStores = schema.stores;\n\n // Verify each store defined in the contract.\n for (const [storeName, contractStore] of Object.entries(contractStores)) {\n const actualStore: IdbStoreIR | undefined = actualStores[storeName];\n storeNodes.push(verifyStore(storeName, contractStore, actualStore, strict, issues));\n }\n\n // Extra stores in manifest (only in strict mode as failures, otherwise warnings).\n for (const [storeName, actualStore] of Object.entries(actualStores)) {\n if (!(storeName in contractStores)) {\n const msg = `Object store \"${storeName}\" exists in manifest but is not in contract`;\n if (strict) {\n issues.push({ kind: \"extra_table\", table: storeName, message: msg, path: [storeName] });\n storeNodes.push(\n failNode(\"collection\", storeName, `storage.stores.${storeName}`, \"extra_table\", msg, undefined, actualStore)\n );\n } else {\n storeNodes.push(\n warnNode(\"collection\", storeName, `storage.stores.${storeName}`, \"extra_table\", msg, undefined, actualStore)\n );\n }\n }\n }\n\n const root: SchemaVerificationNode = {\n status: storeNodes.some((n) => n.status === \"fail\")\n ? \"fail\"\n : storeNodes.some((n) => n.status === \"warn\")\n ? \"warn\"\n : \"pass\",\n kind: \"root\",\n name: \"idb\",\n contractPath: \"storage.stores\",\n code: issues.length === 0 ? \"ok\" : \"schema_mismatch\",\n message: issues.length === 0 ? \"All stores and indexes match\" : `${issues.length} schema issue(s) found`,\n expected: null,\n actual: null,\n children: storeNodes,\n };\n\n // Count statuses including root.\n const allNodes = [root, ...storeNodes];\n const counts = countStatuses(allNodes);\n const ok = root.status !== \"fail\";\n\n const storageHash = (contract.storage as { storageHash: string }).storageHash;\n const profileHash = (contract as { profileHash?: string }).profileHash;\n\n // exactOptionalPropertyTypes: build optional sub-objects conditionally.\n const contractField =\n profileHash !== undefined ? ({ storageHash, profileHash } as const) : ({ storageHash } as const);\n\n const metaField = {\n strict,\n };\n\n const sharedResult = {\n summary: ok ? \"Schema verification passed\" : `Schema verification failed: ${issues.length} issue(s)`,\n contract: contractField,\n target: {\n expected: \"idb\",\n },\n schema: {\n issues,\n root,\n counts,\n },\n meta: metaField,\n timings: { total: Date.now() - start },\n };\n\n if (ok) {\n return { ok: true, ...sharedResult };\n }\n return { ok: false, code: VERIFY_CODE_SCHEMA_FAILURE, ...sharedResult };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-idb/family-idb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Prisma 8 IDB family — control-plane descriptor and family instance (CLI/config integration)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prisma-idb": "./dist/bin/prisma-idb.mjs"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@prisma-idb/target-idb": "0.
|
|
40
|
+
"@prisma-idb/target-idb": "0.10.0",
|
|
41
41
|
"@prisma/cli-engine": "0.6.1",
|
|
42
42
|
"@prisma/orm-framework": "8.0.0-rc.12",
|
|
43
43
|
"@prisma/orm-toolchain": "8.0.0-rc.12",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"preflight-4jDCNvS0.mjs","names":[],"sources":["../src/core/preflight.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { IDBFactory } from \"fake-indexeddb\";\nimport type { MigrationMetadata } from \"@prisma/orm-toolchain/migration-tools/metadata\";\nimport { computeMigrationHash } from \"@prisma/orm-toolchain/migration-tools/hash\";\nimport { chainOrderByMetadata, type ChainablePackage } from \"./chain-order\";\nimport { applyOneDdlOp, isIdbDdlOp, type IdbDdlOp } from \"@prisma-idb/target-idb/migration\";\nimport { join } from \"pathe\";\n\nexport interface PreflightOptions {\n readonly migrationsDir: string;\n /** Output sinks. Default to `process.stdout`/`process.stderr`. */\n readonly out?: (line: string) => void;\n readonly err?: (line: string) => void;\n}\n\n/**\n * Walk every migration package under `<migrationsDir>/app/` in chain order,\n * applying each package's `ops.json` against a fresh `fake-indexeddb`\n * instance inside an `upgradeneeded` callback. Reports per-step status to\n * stdout and the final outcome via exit code.\n *\n * **Why this exists**: a dedicated preflight command avoids three failure modes\n * of inlining this into `db update`: (a) `fake-indexeddb` disagrees with real\n * browsers on some edge cases and is not a reliable production oracle; (b) it\n * would substitute a runtime check for proper test coverage; (c) it would ship\n * a test-only package on a production path. Running it in CI gives the gate\n * without the workflow tax on every author.\n *\n * **Scope vs runtime**: this command catches \"the chain doesn't apply\n * cleanly\" — a structural issue. It does NOT catch \"the chain produces\n * the wrong schema\" (that's `verifySchema` against the head's contract,\n * resolvable via `migrations/snapshots/<hash>/contract.json` since Phase\n * 8.9 — still deferred to a follow-up).\n *\n * Exit codes: 0 on full chain success; 1 on any failure.\n */\nexport async function runPreflight(opts: PreflightOptions): Promise<number> {\n const out = opts.out ?? ((line: string) => process.stdout.write(line));\n const err = opts.err ?? ((line: string) => process.stderr.write(line));\n const migrationsDir = opts.migrationsDir;\n const appDir = join(migrationsDir, \"app\");\n\n const packages = await loadPackages(appDir);\n if (packages.length === 0) {\n out(\"No migration packages found. Nothing to preflight.\\n\");\n return 0;\n }\n\n out(`Preflighting ${packages.length} migration(s) against fake-indexeddb…\\n`);\n\n const factory = new IDBFactory();\n const dbName = \"__preflight__\";\n\n let currentVersion = 0;\n for (const pkg of packages) {\n out(` ${pkg.dirName} … `);\n try {\n currentVersion += 1;\n await applyPackage({ factory, dbName, targetVersion: currentVersion, ops: pkg.ops });\n out(\"ok\\n\");\n } catch (applyErr) {\n out(\"FAILED\\n\");\n err(` ${applyErr instanceof Error ? applyErr.message : String(applyErr)}\\n`);\n err(\"\\nPreflight failed.\\n\");\n return 1;\n }\n }\n\n out(\"\\nPreflight passed: every migration in the chain applies cleanly.\\n\");\n return 0;\n}\n\ninterface LoadedPackage extends ChainablePackage {\n readonly metadata: MigrationMetadata;\n readonly ops: readonly IdbDdlOp[];\n}\n\nasync function loadPackages(appDir: string): Promise<LoadedPackage[]> {\n let dirs: string[];\n try {\n dirs = (await readdir(appDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw err;\n }\n\n // Load all packages unordered, then derive chain order by walking\n // metadata.from/to edges. Directory name sort is unreliable when\n // timestamp formats mix between hand-authored baselines and CLI-emitted\n // additions (e.g. T120000 vs T0337).\n const unordered = new Map<string, LoadedPackage>();\n for (const dirName of dirs) {\n const metaRaw = await readFile(join(appDir, dirName, \"migration.json\"), \"utf-8\");\n const opsRaw = await readFile(join(appDir, dirName, \"ops.json\"), \"utf-8\");\n const metadata = JSON.parse(metaRaw) as MigrationMetadata;\n const opsParsed = JSON.parse(opsRaw) as unknown[];\n const ops: IdbDdlOp[] = [];\n for (const op of opsParsed) {\n if (!isIdbDdlOp(op as never)) {\n throw new Error(`Non-IDB op found in ${dirName}/ops.json: ${JSON.stringify(op)}`);\n }\n ops.push(op as IdbDdlOp);\n }\n const computedHash = computeMigrationHash(metadata, ops as unknown as Parameters<typeof computeMigrationHash>[1]);\n if (computedHash !== metadata.migrationHash) {\n throw new Error(\n `Migration hash mismatch in ${dirName}: stored migrationHash ${metadata.migrationHash} ` +\n `does not match computed hash ${computedHash}`\n );\n }\n unordered.set(dirName, { dirName, metadata, ops });\n }\n\n return chainOrderByMetadata(unordered);\n}\n\nfunction applyPackage(input: {\n readonly factory: IDBFactory;\n readonly dbName: string;\n readonly targetVersion: number;\n readonly ops: readonly IdbDdlOp[];\n}): Promise<void> {\n return new Promise((resolve, reject) => {\n const req = input.factory.open(input.dbName, input.targetVersion);\n req.onupgradeneeded = (event) => {\n const target = event.target as IDBOpenDBRequest;\n const db = target.result;\n const tx = target.transaction;\n if (tx === null) {\n reject(new Error(\"upgradeneeded fired with null version-change transaction\"));\n return;\n }\n try {\n for (const op of input.ops) {\n applyOneDdlOp(db, tx, op);\n }\n } catch (err) {\n reject(err);\n }\n };\n req.onsuccess = () => {\n req.result.close();\n resolve();\n };\n req.onerror = () => reject(req.error ?? new Error(\"preflight open request failed\"));\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,eAAsB,aAAa,MAAyC;CAC1E,MAAM,MAAM,KAAK,SAAS,SAAiB,QAAQ,OAAO,MAAM,IAAI;CACpE,MAAM,MAAM,KAAK,SAAS,SAAiB,QAAQ,OAAO,MAAM,IAAI;CACpE,MAAM,gBAAgB,KAAK;CAG3B,MAAM,WAAW,MAAM,aAFR,KAAK,eAAe,KAEM,CAAC;CAC1C,IAAI,SAAS,WAAW,GAAG;EACzB,IAAI,sDAAsD;EAC1D,OAAO;CACT;CAEA,IAAI,gBAAgB,SAAS,OAAO,wCAAwC;CAE5E,MAAM,UAAU,IAAI,WAAW;CAC/B,MAAM,SAAS;CAEf,IAAI,iBAAiB;CACrB,KAAK,MAAM,OAAO,UAAU;EAC1B,IAAI,KAAK,IAAI,QAAQ,IAAI;EACzB,IAAI;GACF,kBAAkB;GAClB,MAAM,aAAa;IAAE;IAAS;IAAQ,eAAe;IAAgB,KAAK,IAAI;GAAI,CAAC;GACnF,IAAI,MAAM;EACZ,SAAS,UAAU;GACjB,IAAI,UAAU;GACd,IAAI,OAAO,oBAAoB,QAAQ,SAAS,UAAU,OAAO,QAAQ,EAAE,GAAG;GAC9E,IAAI,uBAAuB;GAC3B,OAAO;EACT;CACF;CAEA,IAAI,qEAAqE;CACzE,OAAO;AACT;AAOA,eAAe,aAAa,QAA0C;CACpE,IAAI;CACJ,IAAI;EACF,QAAQ,MAAM,QAAQ,QAAQ,EAAE,eAAe,KAAK,CAAC,EAAA,CAAG,QAAQ,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,IAAI;CAC1G,SAAS,KAAK;EACZ,IAAK,IAA8B,SAAS,UAAU,OAAO,CAAC;EAC9D,MAAM;CACR;CAMA,MAAM,4BAAY,IAAI,IAA2B;CACjD,KAAK,MAAM,WAAW,MAAM;EAC1B,MAAM,UAAU,MAAM,SAAS,KAAK,QAAQ,SAAS,gBAAgB,GAAG,OAAO;EAC/E,MAAM,SAAS,MAAM,SAAS,KAAK,QAAQ,SAAS,UAAU,GAAG,OAAO;EACxE,MAAM,WAAW,KAAK,MAAM,OAAO;EACnC,MAAM,YAAY,KAAK,MAAM,MAAM;EACnC,MAAM,MAAkB,CAAC;EACzB,KAAK,MAAM,MAAM,WAAW;GAC1B,IAAI,CAAC,WAAW,EAAW,GACzB,MAAM,IAAI,MAAM,uBAAuB,QAAQ,aAAa,KAAK,UAAU,EAAE,GAAG;GAElF,IAAI,KAAK,EAAc;EACzB;EACA,MAAM,eAAe,qBAAqB,UAAU,GAA4D;EAChH,IAAI,iBAAiB,SAAS,eAC5B,MAAM,IAAI,MACR,8BAA8B,QAAQ,yBAAyB,SAAS,cAAc,gCACpD,cACpC;EAEF,UAAU,IAAI,SAAS;GAAE;GAAS;GAAU;EAAI,CAAC;CACnD;CAEA,OAAO,qBAAqB,SAAS;AACvC;AAEA,SAAS,aAAa,OAKJ;CAChB,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,MAAM,MAAM,QAAQ,KAAK,MAAM,QAAQ,MAAM,aAAa;EAChE,IAAI,mBAAmB,UAAU;GAC/B,MAAM,SAAS,MAAM;GACrB,MAAM,KAAK,OAAO;GAClB,MAAM,KAAK,OAAO;GAClB,IAAI,OAAO,MAAM;IACf,uBAAO,IAAI,MAAM,0DAA0D,CAAC;IAC5E;GACF;GACA,IAAI;IACF,KAAK,MAAM,MAAM,MAAM,KACrB,cAAc,IAAI,IAAI,EAAE;GAE5B,SAAS,KAAK;IACZ,OAAO,GAAG;GACZ;EACF;EACA,IAAI,kBAAkB;GACpB,IAAI,OAAO,MAAM;GACjB,QAAQ;EACV;EACA,IAAI,gBAAgB,OAAO,IAAI,yBAAS,IAAI,MAAM,+BAA+B,CAAC;CACpF,CAAC;AACH"}
|