@prisma-idb/family-idb 0.6.1 → 0.8.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.
Files changed (31) hide show
  1. package/README.md +25 -16
  2. package/dist/bin/prisma-idb.mjs +1 -1
  3. package/dist/{cli-CsJ7c40P.mjs → cli-JTFu1R3s.mjs} +11 -21
  4. package/dist/cli-JTFu1R3s.mjs.map +1 -0
  5. package/dist/exports/cli.mjs +1 -1
  6. package/dist/exports/contract-psl.d.mts +1 -1
  7. package/dist/exports/contract-psl.d.mts.map +1 -1
  8. package/dist/exports/contract-psl.mjs +7 -15
  9. package/dist/exports/contract-psl.mjs.map +1 -1
  10. package/dist/exports/contract-ts.d.mts +24 -11
  11. package/dist/exports/contract-ts.d.mts.map +1 -1
  12. package/dist/exports/contract-ts.mjs +108 -30
  13. package/dist/exports/contract-ts.mjs.map +1 -1
  14. package/dist/exports/control.d.mts +15 -3
  15. package/dist/exports/control.d.mts.map +1 -1
  16. package/dist/exports/control.mjs +17 -6
  17. package/dist/exports/control.mjs.map +1 -1
  18. package/dist/{migration-plan-BWhd8BdV.mjs → migration-plan-BwakybVZ.mjs} +8 -2
  19. package/dist/migration-plan-BwakybVZ.mjs.map +1 -0
  20. package/dist/{psl-interpreter-C6Q4okuO.mjs → psl-interpreter-BoPLi4c4.mjs} +230 -112
  21. package/dist/psl-interpreter-BoPLi4c4.mjs.map +1 -0
  22. package/dist/{psl-interpreter-C0suaXu2.d.mts → psl-interpreter-Vi1cBmhb.d.mts} +5 -3
  23. package/dist/psl-interpreter-Vi1cBmhb.d.mts.map +1 -0
  24. package/dist/{validate-BLnItEKs.mjs → validate-dwKE5_5B.mjs} +16 -3
  25. package/dist/validate-dwKE5_5B.mjs.map +1 -0
  26. package/package.json +5 -5
  27. package/dist/cli-CsJ7c40P.mjs.map +0 -1
  28. package/dist/migration-plan-BWhd8BdV.mjs.map +0 -1
  29. package/dist/psl-interpreter-C0suaXu2.d.mts.map +0 -1
  30. package/dist/psl-interpreter-C6Q4okuO.mjs.map +0 -1
  31. package/dist/validate-BLnItEKs.mjs.map +0 -1
package/README.md CHANGED
@@ -1,29 +1,38 @@
1
1
  # `@prisma-idb/family-idb`
2
2
 
3
- > The [Prisma 8](https://www.prisma.io/blog/prisma-next-call-for-extension-authors) driver + adapter + target stack for IndexedDB — offline-first, no server required.
3
+ The IndexedDB family for Prisma 8, used at build time. It reads your Prisma schema into an IndexedDB contract, plugs IndexedDB into the `prisma` CLI, and provides the `prisma-idb` CLI for migrations.
4
4
 
5
- **family-idb** is the main entry point for the `@prisma-idb` stack. It exposes the control-plane descriptor, the family instance, and the `prisma-idb` CLI for migration authoring and config integration. The former `prisma-idb` command remains an alias.
5
+ ```bash
6
+ npm install --save-dev @prisma-idb/family-idb
7
+ ```
6
8
 
7
- ## Stack
9
+ ## The `prisma-idb` CLI
8
10
 
9
- ```
10
- family-idb ← you are here (CLI / config / entry point)
11
- client-idb (ORM query builder)
12
- runtime-idb (RuntimeCore)
13
- adapter-idb (query AST → IDB plan)
14
- driver-idb (window.indexedDB wrapper)
15
- target-idb (identity + migrations)
16
- ```
11
+ IndexedDB only exists in the browser, so three migration jobs need their own commands:
17
12
 
18
- ## Installation
13
+ | Command | Does |
14
+ | ------------------------------------- | ------------------------------------------------------------------------------- |
15
+ | `prisma-idb migration plan` | Plans the next migration from the newest one on disk. Warns if it deletes data. |
16
+ | `prisma-idb migration contract-space` | Bundles the migrations into a module the browser imports. |
17
+ | `prisma-idb migration preflight` | Applies every migration to an in-memory IndexedDB to check that it runs. |
19
18
 
20
- ```bash
21
- npm install @prisma-idb/family-idb
22
- ```
19
+ All three read `prisma.config.ts`. See [Migrations](https://prisma-idb.dev/docs/prisma-8/migrations).
20
+
21
+ ## Entry points
22
+
23
+ | Import | Contains |
24
+ | ------------------------------------- | -------------------------------------------------------------------- |
25
+ | `@prisma-idb/family-idb/control` | The family descriptor for `prisma.config.ts`. |
26
+ | `@prisma-idb/family-idb/config-types` | `defineConfig` for `prisma.config.ts`. |
27
+ | `@prisma-idb/family-idb/contract-psl` | `prismaIdbContract(path, { projection })`: reads a `.prisma` schema. |
28
+ | `@prisma-idb/family-idb/contract-ts` | `defineContract()`: write a contract in TypeScript instead. |
29
+ | `@prisma-idb/family-idb/cli` | The `prisma-idb` commands, for embedding in another CLI. |
23
30
 
24
31
  ## Documentation
25
32
 
26
- See [prisma-idb.dev](https://prisma-idb.dev/) for full setup and usage docs.
33
+ - [Quick Start](https://prisma-idb.dev/docs/prisma-8/getting-started)
34
+ - [Schema & Config](https://prisma-idb.dev/docs/prisma-8/schema)
35
+ - [Migrations](https://prisma-idb.dev/docs/prisma-8/migrations)
27
36
 
28
37
  ## License
29
38
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as runIdbCli } from "../cli-CsJ7c40P.mjs";
2
+ import { i as runIdbCli } from "../cli-JTFu1R3s.mjs";
3
3
  //#region src/bin/prisma-idb.ts
4
4
  runIdbCli(process).then((code) => {
5
5
  process.exitCode = code;
@@ -2,7 +2,6 @@ import { dirname, join, resolve } from "pathe";
2
2
  import { createCli, defineCommand, defineCommandFamily, flag, loadConfig } from "@prisma/cli-engine";
3
3
  import { ormConfigSection } from "@prisma/orm-toolchain/cli";
4
4
  import { CliStructuredError, notOk, ok } from "@prisma/cli-engine/protocol";
5
- import { finalizeConfig } from "@prisma/orm-toolchain/config-loader";
6
5
  //#region src/cli/collecting-sink.ts
7
6
  /**
8
7
  * Buffers writes the way a real stream does: a write with no trailing
@@ -55,13 +54,6 @@ function normalizeError(error) {
55
54
  if (CliStructuredError.is(error)) return error;
56
55
  return new CliStructuredError("IDB-CLI.UNEXPECTED_ERROR", error instanceof Error ? error.message : String(error));
57
56
  }
58
- function finalizedConfigContext(ctx) {
59
- if (ctx.config === void 0) return ctx;
60
- return {
61
- ...ctx,
62
- config: finalizeConfig(ctx.config, ctx.cwd)
63
- };
64
- }
65
57
  /**
66
58
  * These 3 commands are IDB-specific (fake-indexeddb, IdbMigrationPlanner) —
67
59
  * pointing them at a non-IDB `prisma.config.ts` would fail confusingly deep
@@ -81,12 +73,11 @@ function requireIdbFamily(config) {
81
73
  }
82
74
  /**
83
75
  * `defineCommand` pre-bound to the `orm` config section (see
84
- * `config-section.ts`) with a uniform error boundary and path
85
- * finalization, mirroring `@prisma/orm-toolchain`'s own `defineOrmCommand`
86
- * (`vendor/prisma/packages/1-framework/3-tooling/cli/src/orm/define-command.ts`):
87
- * the engine's config loader hands a command `contract.output`/
88
- * `migrations.dir` exactly as authored (usually relative); finalizing here,
89
- * once, means handlers always read absolute paths.
76
+ * `config-section.ts`) with a uniform error boundary, mirroring
77
+ * `@prisma/orm-toolchain`'s own `defineOrmCommand`. The section's schema
78
+ * resolves `contract.output`/`migrations.dir` against the config file that
79
+ * wrote them (and defaults `migrations.dir`), so handlers always read
80
+ * absolute paths without a separate finalize step.
90
81
  */
91
82
  function defineIdbCommand(def) {
92
83
  return defineCommand({
@@ -94,9 +85,8 @@ function defineIdbCommand(def) {
94
85
  needs: { config: ormConfigSection },
95
86
  handler: async (args, ctx) => {
96
87
  try {
97
- const finalized = finalizedConfigContext(ctx);
98
- requireIdbFamily(finalized.config);
99
- return await def.handler(args, finalized);
88
+ requireIdbFamily(ctx.config);
89
+ return await def.handler(args, ctx);
100
90
  } catch (error) {
101
91
  return notOk(normalizeError(error));
102
92
  }
@@ -196,7 +186,7 @@ const commands = {
196
186
  placeholder: "slug"
197
187
  }),
198
188
  space: flag.string({
199
- brief: "Contract-space id (default: \"app\"). See ADR 212 for extension-space migrations.",
189
+ brief: "Contract-space id (default: \"app\"). Only extension authors set this.",
200
190
  placeholder: "id"
201
191
  }),
202
192
  contract: flag.string({
@@ -211,7 +201,7 @@ const commands = {
211
201
  handler: async (args, ctx) => {
212
202
  const contractPath = requireContractPath(ctx.config, ctx.cwd, args.flags.contract);
213
203
  const migrationsDir = resolveMigrationsDir(ctx.config, ctx.cwd, args.flags.migrationsDir);
214
- const { migrationPlan } = await import("./migration-plan-BWhd8BdV.mjs");
204
+ const { migrationPlan } = await import("./migration-plan-BwakybVZ.mjs");
215
205
  const sink = createCollectingSink();
216
206
  const exitCode = await migrationPlan({
217
207
  migrationsDir,
@@ -265,7 +255,7 @@ const idbCommandFamily = defineCommandFamily({
265
255
  });
266
256
  //#endregion
267
257
  //#region package.json
268
- var version = "0.6.1";
258
+ var version = "0.8.0";
269
259
  //#endregion
270
260
  //#region src/cli/cli.ts
271
261
  const BIN_NAME = "prisma-idb";
@@ -339,4 +329,4 @@ async function runIdbCli(proc) {
339
329
  //#endregion
340
330
  export { idbCommandFamily as a, runIdbCli as i, BIN_NAME as n, createIdbCli as r, BIN_GROUPS as t };
341
331
 
342
- //# sourceMappingURL=cli-CsJ7c40P.mjs.map
332
+ //# sourceMappingURL=cli-JTFu1R3s.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-JTFu1R3s.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":"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"}
@@ -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-CsJ7c40P.mjs";
1
+ import { a as idbCommandFamily, i as runIdbCli, n as BIN_NAME, r as createIdbCli, t as BIN_GROUPS } from "../cli-JTFu1R3s.mjs";
2
2
  export { BIN_GROUPS, BIN_NAME, createIdbCli, idbCommandFamily, runIdbCli };
@@ -1,4 +1,4 @@
1
- import { n as InterpretPslOptions, r as interpretPslDocumentToIdbContract, t as ContractProjection } from "../psl-interpreter-C0suaXu2.mjs";
1
+ import { n as InterpretPslOptions, r as interpretPslDocumentToIdbContract, t as ContractProjection } from "../psl-interpreter-Vi1cBmhb.mjs";
2
2
  import { ContractConfig } from "@prisma/orm-framework/config/config-types";
3
3
  //#region src/core/psl-provider.d.ts
4
4
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"contract-psl.d.mts","names":[],"sources":["../../src/core/psl-provider.ts"],"mappings":";;;;;;;;;;;;;;;iBAuBgB,0BAA0B;UA6BzB;WACN;;;;;;;WAOA,aAAa;;;;;;;;;WASb,oBAAoB;;;;;;;;;;;;;;;;;;;;;;iBAuBf,kBAAkB,oBAAoB,UAAU,2BAA2B"}
1
+ {"version":3,"file":"contract-psl.d.mts","names":[],"sources":["../../src/core/psl-provider.ts"],"mappings":";;;;;;;;;;;;;;;iBAsBgB,0BAA0B;UAgBzB;WACN;;;;;;;WAOA,aAAa;;;;;;;;;WASb,oBAAoB;;;;;;;;;;;;;;;;;;;;;;iBAuBf,kBAAkB,oBAAoB,UAAU,2BAA2B"}
@@ -1,7 +1,7 @@
1
- import { t as interpretPslDocumentToIdbContract } from "../psl-interpreter-C6Q4okuO.mjs";
1
+ import { t as interpretPslDocumentToIdbContract } from "../psl-interpreter-BoPLi4c4.mjs";
2
2
  import { notOk } from "@prisma/orm-framework/utils/result";
3
3
  import { readFile } from "node:fs/promises";
4
- import { buildSymbolTable, rangeToPslSpan } from "@prisma/orm-framework/psl-parser";
4
+ import { buildSymbolTable, mapPslDiagnostics } from "@prisma/orm-framework/psl-parser";
5
5
  import { withSeedDiagnostics } from "@prisma/orm-framework/psl-parser/interpret";
6
6
  import { parse } from "@prisma/orm-framework/psl-parser/syntax";
7
7
  import { basename, extname } from "pathe";
@@ -28,14 +28,6 @@ function defaultOutputFromSchemaPath(schemaPath) {
28
28
  if (basename(base) === "schema") return `${base.slice(0, -6)}contract.json`;
29
29
  return `${base}.json`;
30
30
  }
31
- function mapParseDiagnostics(diagnostics, sourceFile, sourceId) {
32
- return diagnostics.map((d) => ({
33
- code: d.code,
34
- message: d.message,
35
- sourceId,
36
- span: rangeToPslSpan(d.range, sourceFile)
37
- }));
38
- }
39
31
  /**
40
32
  * Creates a `ContractConfig` that reads an IDB schema from a `.prisma` file.
41
33
  *
@@ -78,13 +70,13 @@ function prismaIdbContract(schemaPath, options) {
78
70
  });
79
71
  }
80
72
  if (options?.injectSchemaText) schema = options.injectSchemaText(schema);
81
- const { document, sourceFile, diagnostics: parseDiagnostics } = parse(schema);
82
- const { table, diagnostics: symbolDiagnostics } = buildSymbolTable({
83
- document,
84
- sourceFile,
73
+ const { document, sources, diagnostics: parseDiagnostics } = parse(schema, schemaPath);
74
+ const { symbolTable: table, diagnostics: symbolDiagnostics } = buildSymbolTable({
75
+ documents: [document],
76
+ sources,
85
77
  pslBlockDescriptors: {}
86
78
  });
87
- const seedDiagnostics = [...mapParseDiagnostics(parseDiagnostics, sourceFile, schemaPath), ...mapParseDiagnostics(symbolDiagnostics, sourceFile, schemaPath)];
79
+ const seedDiagnostics = [...mapPslDiagnostics(parseDiagnostics, sources), ...mapPslDiagnostics(symbolDiagnostics, sources)];
88
80
  return withSeedDiagnostics(interpretPslDocumentToIdbContract(table, schemaPath, options?.projection !== void 0 ? { projection: options.projection } : void 0), seedDiagnostics);
89
81
  }
90
82
  },
@@ -1 +1 @@
1
- {"version":3,"file":"contract-psl.mjs","names":[],"sources":["../../src/core/psl-provider.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport type { ContractConfig, ContractSourceDiagnostic } from \"@prisma/orm-framework/config/config-types\";\nimport { buildSymbolTable, rangeToPslSpan } from \"@prisma/orm-framework/psl-parser\";\nimport { withSeedDiagnostics } from \"@prisma/orm-framework/psl-parser/interpret\";\nimport type { ParseDiagnostic, SourceFile } from \"@prisma/orm-framework/psl-parser/syntax\";\nimport { parse } from \"@prisma/orm-framework/psl-parser/syntax\";\nimport { notOk } from \"@prisma/orm-framework/utils/result\";\nimport { extname, basename } from \"pathe\";\nimport type { ContractProjection } from \"./psl-interpreter\";\nimport { interpretPslDocumentToIdbContract } from \"./psl-interpreter\";\n\n/**\n * Removes `@idb.exclude`/`@@idb.exclude` from raw PSL text — the `idb`\n * namespace family-idb owns (`psl-interpreter.ts`'s `IDB_EXCLUDE_ATTR`).\n * A foreign PSL parser (e.g. the SQL family's) doesn't recognize this\n * namespace and hard-errors on it (`PSL_EXTENSION_NAMESPACE_NOT_COMPOSED`);\n * stripping it is the text-level equivalent of what family-idb itself\n * already does for these same fields under `projection: \"full\"`, where the\n * attribute is documented as a no-op. Use this when sharing a schema.prisma\n * authored for family-idb with a different family's schema loader — the\n * server always wants every field these attributes would otherwise hide\n * from the client.\n */\nexport function stripIdbExcludeAttributes(schema: string): string {\n return schema\n .replace(/^[ \\t]*@@idb\\.exclude\\b.*\\n?/gm, \"\") // model-level: drop the whole line, incl. trailing comments\n .replace(/[ \\t]*@idb\\.exclude\\b/g, \"\"); // field-level: drop just the token, keep the field\n}\n\nfunction defaultOutputFromSchemaPath(schemaPath: string): string {\n const ext = extname(schemaPath);\n if (ext.length === 0) return `${schemaPath}.json`;\n const base = schemaPath.slice(0, -ext.length);\n if (basename(base) === \"schema\") {\n return `${base.slice(0, -\"schema\".length)}contract.json`;\n }\n return `${base}.json`;\n}\n\nfunction mapParseDiagnostics(\n diagnostics: readonly ParseDiagnostic[],\n sourceFile: SourceFile,\n sourceId: string\n): ContractSourceDiagnostic[] {\n return diagnostics.map((d) => ({\n code: d.code as string,\n message: d.message,\n sourceId,\n span: rangeToPslSpan(d.range, sourceFile),\n }));\n}\n\nexport interface PrismaIdbContractOptions {\n readonly output?: string;\n /**\n * `\"client\"` projects out anything marked `@idb.exclude`/`@@idb.exclude`\n * (ADR 012). Pass a distinct `output` alongside this so the client and\n * full/server contracts don't collide on the same file.\n * @default \"full\"\n */\n readonly projection?: ContractProjection;\n /**\n * Runs on the raw schema text right after it's read, before parsing.\n * Lets a downstream package (e.g. `@prisma-idb/sync-server`) append\n * synthetic model/enum declarations — the appended text goes through the\n * exact same parse → interpret → hash pipeline as hand-authored PSL, so\n * `storageHash` reflects it correctly (it's computed *inside*\n * `interpretPslDocumentToIdbContract`, from whatever this returns).\n */\n readonly injectSchemaText?: (schema: string) => string;\n}\n\n/**\n * Creates a `ContractConfig` that reads an IDB schema from a `.prisma` file.\n *\n * Use this in `prisma.config.ts` as the `contract:` value when you prefer\n * PSL authoring over the TypeScript-first `defineContract()` helper.\n *\n * @example\n * ```ts\n * import { defineConfig } from '@prisma-idb/family-idb/config-types';\n * import { prismaIdbContract } from '@prisma-idb/family-idb/contract-psl';\n *\n * export default defineConfig({\n * // ...\n * contract: prismaIdbContract('./src/prisma/schema.prisma'),\n * });\n * ```\n *\n * The emitted `contract.json` lands next to the schema file by default\n * (`schema.prisma` → `contract.json`). Override with `options.output`.\n */\nexport function prismaIdbContract(schemaPath: string, options?: PrismaIdbContractOptions): ContractConfig {\n return {\n source: {\n inputs: [schemaPath],\n load: async (context) => {\n const [absoluteSchemaPath] = context.resolvedInputs;\n if (absoluteSchemaPath === undefined) {\n throw new Error(\n \"prismaIdbContract: context.resolvedInputs is empty. The CLI config loader should populate it positional-matched with source.inputs.\"\n );\n }\n\n let schema: string;\n try {\n schema = await readFile(absoluteSchemaPath, \"utf-8\");\n } catch (error) {\n const message = String(error);\n return notOk({\n summary: `Failed to read Prisma schema at \"${schemaPath}\"`,\n diagnostics: [\n {\n code: \"PSL_SCHEMA_READ_FAILED\",\n message,\n sourceId: schemaPath,\n },\n ],\n });\n }\n\n if (options?.injectSchemaText) {\n schema = options.injectSchemaText(schema);\n }\n\n const { document, sourceFile, diagnostics: parseDiagnostics } = parse(schema);\n const { table, diagnostics: symbolDiagnostics } = buildSymbolTable({\n document,\n sourceFile,\n pslBlockDescriptors: {},\n });\n\n // Do not short-circuit on provider-level diagnostics — the fault-tolerant\n // parser/symbol-table still produce a usable table, and the interpreter\n // may surface its own diagnostics in the same response.\n const seedDiagnostics = [\n ...mapParseDiagnostics(parseDiagnostics, sourceFile, schemaPath),\n ...mapParseDiagnostics(symbolDiagnostics, sourceFile, schemaPath),\n ];\n\n const interpreted = withSeedDiagnostics(\n interpretPslDocumentToIdbContract(\n table,\n schemaPath,\n options?.projection !== undefined ? { projection: options.projection } : undefined\n ),\n seedDiagnostics\n );\n // Return the full Result so seed diagnostics (parse + symbol-table\n // findings) survive the success path — the config loader surfaces\n // them alongside the contract.\n return interpreted;\n },\n },\n output: options?.output ?? defaultOutputFromSchemaPath(schemaPath),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,0BAA0B,QAAwB;CAChE,OAAO,OACJ,QAAQ,kCAAkC,EAAE,CAAC,CAC7C,QAAQ,0BAA0B,EAAE;AACzC;AAEA,SAAS,4BAA4B,YAA4B;CAC/D,MAAM,MAAM,QAAQ,UAAU;CAC9B,IAAI,IAAI,WAAW,GAAG,OAAO,GAAG,WAAW;CAC3C,MAAM,OAAO,WAAW,MAAM,GAAG,CAAC,IAAI,MAAM;CAC5C,IAAI,SAAS,IAAI,MAAM,UACrB,OAAO,GAAG,KAAK,MAAM,GAAG,EAAgB,EAAE;CAE5C,OAAO,GAAG,KAAK;AACjB;AAEA,SAAS,oBACP,aACA,YACA,UAC4B;CAC5B,OAAO,YAAY,KAAK,OAAO;EAC7B,MAAM,EAAE;EACR,SAAS,EAAE;EACX;EACA,MAAM,eAAe,EAAE,OAAO,UAAU;CAC1C,EAAE;AACJ;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,kBAAkB,YAAoB,SAAoD;CACxG,OAAO;EACL,QAAQ;GACN,QAAQ,CAAC,UAAU;GACnB,MAAM,OAAO,YAAY;IACvB,MAAM,CAAC,sBAAsB,QAAQ;IACrC,IAAI,uBAAuB,KAAA,GACzB,MAAM,IAAI,MACR,qIACF;IAGF,IAAI;IACJ,IAAI;KACF,SAAS,MAAM,SAAS,oBAAoB,OAAO;IACrD,SAAS,OAAO;KACd,MAAM,UAAU,OAAO,KAAK;KAC5B,OAAO,MAAM;MACX,SAAS,oCAAoC,WAAW;MACxD,aAAa,CACX;OACE,MAAM;OACN;OACA,UAAU;MACZ,CACF;KACF,CAAC;IACH;IAEA,IAAI,SAAS,kBACX,SAAS,QAAQ,iBAAiB,MAAM;IAG1C,MAAM,EAAE,UAAU,YAAY,aAAa,qBAAqB,MAAM,MAAM;IAC5E,MAAM,EAAE,OAAO,aAAa,sBAAsB,iBAAiB;KACjE;KACA;KACA,qBAAqB,CAAC;IACxB,CAAC;IAKD,MAAM,kBAAkB,CACtB,GAAG,oBAAoB,kBAAkB,YAAY,UAAU,GAC/D,GAAG,oBAAoB,mBAAmB,YAAY,UAAU,CAClE;IAaA,OAXoB,oBAClB,kCACE,OACA,YACA,SAAS,eAAe,KAAA,IAAY,EAAE,YAAY,QAAQ,WAAW,IAAI,KAAA,CAC3E,GACA,eAKe;GACnB;EACF;EACA,QAAQ,SAAS,UAAU,4BAA4B,UAAU;CACnE;AACF"}
1
+ {"version":3,"file":"contract-psl.mjs","names":[],"sources":["../../src/core/psl-provider.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport type { ContractConfig } from \"@prisma/orm-framework/config/config-types\";\nimport { buildSymbolTable, mapPslDiagnostics } from \"@prisma/orm-framework/psl-parser\";\nimport { withSeedDiagnostics } from \"@prisma/orm-framework/psl-parser/interpret\";\nimport { parse } from \"@prisma/orm-framework/psl-parser/syntax\";\nimport { notOk } from \"@prisma/orm-framework/utils/result\";\nimport { extname, basename } from \"pathe\";\nimport type { ContractProjection } from \"./psl-interpreter\";\nimport { interpretPslDocumentToIdbContract } from \"./psl-interpreter\";\n\n/**\n * Removes `@idb.exclude`/`@@idb.exclude` from raw PSL text — the `idb`\n * namespace family-idb owns (`psl-interpreter.ts`'s `IDB_EXCLUDE_ATTR`).\n * A foreign PSL parser (e.g. the SQL family's) doesn't recognize this\n * namespace and hard-errors on it (`PSL_EXTENSION_NAMESPACE_NOT_COMPOSED`);\n * stripping it is the text-level equivalent of what family-idb itself\n * already does for these same fields under `projection: \"full\"`, where the\n * attribute is documented as a no-op. Use this when sharing a schema.prisma\n * authored for family-idb with a different family's schema loader — the\n * server always wants every field these attributes would otherwise hide\n * from the client.\n */\nexport function stripIdbExcludeAttributes(schema: string): string {\n return schema\n .replace(/^[ \\t]*@@idb\\.exclude\\b.*\\n?/gm, \"\") // model-level: drop the whole line, incl. trailing comments\n .replace(/[ \\t]*@idb\\.exclude\\b/g, \"\"); // field-level: drop just the token, keep the field\n}\n\nfunction defaultOutputFromSchemaPath(schemaPath: string): string {\n const ext = extname(schemaPath);\n if (ext.length === 0) return `${schemaPath}.json`;\n const base = schemaPath.slice(0, -ext.length);\n if (basename(base) === \"schema\") {\n return `${base.slice(0, -\"schema\".length)}contract.json`;\n }\n return `${base}.json`;\n}\n\nexport interface PrismaIdbContractOptions {\n readonly output?: string;\n /**\n * `\"client\"` projects out anything marked `@idb.exclude`/`@@idb.exclude`\n * (ADR 012). Pass a distinct `output` alongside this so the client and\n * full/server contracts don't collide on the same file.\n * @default \"full\"\n */\n readonly projection?: ContractProjection;\n /**\n * Runs on the raw schema text right after it's read, before parsing.\n * Lets a downstream package (e.g. `@prisma-idb/sync-server`) append\n * synthetic model/enum declarations — the appended text goes through the\n * exact same parse → interpret → hash pipeline as hand-authored PSL, so\n * `storageHash` reflects it correctly (it's computed *inside*\n * `interpretPslDocumentToIdbContract`, from whatever this returns).\n */\n readonly injectSchemaText?: (schema: string) => string;\n}\n\n/**\n * Creates a `ContractConfig` that reads an IDB schema from a `.prisma` file.\n *\n * Use this in `prisma.config.ts` as the `contract:` value when you prefer\n * PSL authoring over the TypeScript-first `defineContract()` helper.\n *\n * @example\n * ```ts\n * import { defineConfig } from '@prisma-idb/family-idb/config-types';\n * import { prismaIdbContract } from '@prisma-idb/family-idb/contract-psl';\n *\n * export default defineConfig({\n * // ...\n * contract: prismaIdbContract('./src/prisma/schema.prisma'),\n * });\n * ```\n *\n * The emitted `contract.json` lands next to the schema file by default\n * (`schema.prisma` → `contract.json`). Override with `options.output`.\n */\nexport function prismaIdbContract(schemaPath: string, options?: PrismaIdbContractOptions): ContractConfig {\n return {\n source: {\n inputs: [schemaPath],\n load: async (context) => {\n const [absoluteSchemaPath] = context.resolvedInputs;\n if (absoluteSchemaPath === undefined) {\n throw new Error(\n \"prismaIdbContract: context.resolvedInputs is empty. The CLI config loader should populate it positional-matched with source.inputs.\"\n );\n }\n\n let schema: string;\n try {\n schema = await readFile(absoluteSchemaPath, \"utf-8\");\n } catch (error) {\n const message = String(error);\n return notOk({\n summary: `Failed to read Prisma schema at \"${schemaPath}\"`,\n diagnostics: [\n {\n code: \"PSL_SCHEMA_READ_FAILED\",\n message,\n sourceId: schemaPath,\n },\n ],\n });\n }\n\n if (options?.injectSchemaText) {\n schema = options.injectSchemaText(schema);\n }\n\n // The filename becomes each diagnostic's `sourceId`, so pass the\n // configured schema path to keep diagnostics pointing at the user's file.\n const { document, sources, diagnostics: parseDiagnostics } = parse(schema, schemaPath);\n const { symbolTable: table, diagnostics: symbolDiagnostics } = buildSymbolTable({\n documents: [document],\n sources,\n pslBlockDescriptors: {},\n });\n\n // Do not short-circuit on provider-level diagnostics — the fault-tolerant\n // parser/symbol-table still produce a usable table, and the interpreter\n // may surface its own diagnostics in the same response.\n const seedDiagnostics = [\n ...mapPslDiagnostics(parseDiagnostics, sources),\n ...mapPslDiagnostics(symbolDiagnostics, sources),\n ];\n\n const interpreted = withSeedDiagnostics(\n interpretPslDocumentToIdbContract(\n table,\n schemaPath,\n options?.projection !== undefined ? { projection: options.projection } : undefined\n ),\n seedDiagnostics\n );\n // Return the full Result so seed diagnostics (parse + symbol-table\n // findings) survive the success path — the config loader surfaces\n // them alongside the contract.\n return interpreted;\n },\n },\n output: options?.output ?? defaultOutputFromSchemaPath(schemaPath),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,0BAA0B,QAAwB;CAChE,OAAO,OACJ,QAAQ,kCAAkC,EAAE,CAAC,CAC7C,QAAQ,0BAA0B,EAAE;AACzC;AAEA,SAAS,4BAA4B,YAA4B;CAC/D,MAAM,MAAM,QAAQ,UAAU;CAC9B,IAAI,IAAI,WAAW,GAAG,OAAO,GAAG,WAAW;CAC3C,MAAM,OAAO,WAAW,MAAM,GAAG,CAAC,IAAI,MAAM;CAC5C,IAAI,SAAS,IAAI,MAAM,UACrB,OAAO,GAAG,KAAK,MAAM,GAAG,EAAgB,EAAE;CAE5C,OAAO,GAAG,KAAK;AACjB;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,kBAAkB,YAAoB,SAAoD;CACxG,OAAO;EACL,QAAQ;GACN,QAAQ,CAAC,UAAU;GACnB,MAAM,OAAO,YAAY;IACvB,MAAM,CAAC,sBAAsB,QAAQ;IACrC,IAAI,uBAAuB,KAAA,GACzB,MAAM,IAAI,MACR,qIACF;IAGF,IAAI;IACJ,IAAI;KACF,SAAS,MAAM,SAAS,oBAAoB,OAAO;IACrD,SAAS,OAAO;KACd,MAAM,UAAU,OAAO,KAAK;KAC5B,OAAO,MAAM;MACX,SAAS,oCAAoC,WAAW;MACxD,aAAa,CACX;OACE,MAAM;OACN;OACA,UAAU;MACZ,CACF;KACF,CAAC;IACH;IAEA,IAAI,SAAS,kBACX,SAAS,QAAQ,iBAAiB,MAAM;IAK1C,MAAM,EAAE,UAAU,SAAS,aAAa,qBAAqB,MAAM,QAAQ,UAAU;IACrF,MAAM,EAAE,aAAa,OAAO,aAAa,sBAAsB,iBAAiB;KAC9E,WAAW,CAAC,QAAQ;KACpB;KACA,qBAAqB,CAAC;IACxB,CAAC;IAKD,MAAM,kBAAkB,CACtB,GAAG,kBAAkB,kBAAkB,OAAO,GAC9C,GAAG,kBAAkB,mBAAmB,OAAO,CACjD;IAaA,OAXoB,oBAClB,kCACE,OACA,YACA,SAAS,eAAe,KAAA,IAAY,EAAE,YAAY,QAAQ,WAAW,IAAI,KAAA,CAC3E,GACA,eAKe;GACnB;EACF;EACA,QAAQ,SAAS,UAAU,4BAA4B,UAAU;CACnE;AACF"}
@@ -1,13 +1,17 @@
1
- import { t as ContractProjection } from "../psl-interpreter-C0suaXu2.mjs";
1
+ import { t as ContractProjection } from "../psl-interpreter-Vi1cBmhb.mjs";
2
+ import { IdbKeyPath, IdbReferentialAction, IdbStorage } from "@prisma-idb/target-idb/pack";
2
3
  import { Contract } from "@prisma/orm-framework/contract/types";
3
- import { IdbReferentialAction, IdbStorage } from "@prisma-idb/target-idb/pack";
4
4
  //#region src/core/contract-builder.d.ts
5
5
  type PrismaScalarType = "String" | "Int" | "Float" | "Boolean" | "DateTime" | "BigInt" | "Decimal" | "Json" | "Bytes";
6
+ type PrismaScalarFieldSpec = PrismaScalarType | `${PrismaScalarType}?`;
6
7
  /**
7
8
  * A field spec string: the Prisma scalar type name, optionally suffixed with
8
9
  * `?` to indicate the field is nullable (e.g. `"String"`, `"Int?"`, `"DateTime?"`).
9
10
  */
10
- type FieldSpec = PrismaScalarType | `${PrismaScalarType}?`;
11
+ type FieldSpec<EnumName extends string = never> = PrismaScalarFieldSpec | EnumName | `${EnumName}?` | `${EnumName}[]`;
12
+ /** A named enum's ordered string values. Each value is also its member name. */
13
+ type EnumDef = readonly string[];
14
+ type EnumDefs = Readonly<Record<string, EnumDef>>;
11
15
  type RelationDef = {
12
16
  readonly to: string;
13
17
  readonly cardinality: "1:1" | "1:N" | "N:1";
@@ -17,17 +21,25 @@ type RelationDef = {
17
21
  };
18
22
  readonly onDelete?: IdbReferentialAction;
19
23
  readonly onUpdate?: IdbReferentialAction;
24
+ /**
25
+ * Whether a to-one (`N:1` / `1:1`) relation can be absent. Defaults to
26
+ * `true` when any `on.local` field is nullable (`"String?"`), `false`
27
+ * otherwise. Ignored for `1:N`.
28
+ */
29
+ readonly nullable?: boolean;
20
30
  };
21
31
  type IndexDef = {
22
- readonly keyPath: string;
32
+ /** A single field name, or an ordered list of field names for a compound (multi-field) index. */
33
+ readonly keyPath: IdbKeyPath;
23
34
  readonly unique?: boolean;
24
35
  readonly multiEntry?: boolean;
25
36
  };
26
- type ModelDef = {
37
+ type ModelDef<EnumName extends string = never> = {
27
38
  readonly store: string;
28
- readonly key: string;
39
+ /** The primary key field, or an ordered list of fields for a compound primary key. */
40
+ readonly key: IdbKeyPath;
29
41
  /** All scalar fields on the model. Use `"Type"` for non-nullable, `"Type?"` for nullable. */
30
- readonly fields: Record<string, FieldSpec>;
42
+ readonly fields: Record<string, FieldSpec<EnumName>>;
31
43
  readonly indexes?: Record<string, IndexDef>;
32
44
  readonly relations?: Record<string, RelationDef>;
33
45
  /**
@@ -48,7 +60,7 @@ type ModelDef = {
48
60
  /** Server-only fields on an otherwise-synced model — dropped in client projection. See ADR 012. */
49
61
  readonly excludeFields?: readonly string[];
50
62
  };
51
- type DefineContractInput = {
63
+ type DefineContractInput<TEnums extends EnumDefs = Record<never, never>> = {
52
64
  /** Pass the default export of `@prisma-idb/family-idb/pack`. */
53
65
  readonly family: {
54
66
  readonly familyId: "idb";
@@ -59,7 +71,8 @@ type DefineContractInput = {
59
71
  readonly targetId: string;
60
72
  readonly id: string;
61
73
  };
62
- readonly models: Record<string, ModelDef>;
74
+ readonly enums?: TEnums;
75
+ readonly models: Record<string, ModelDef<Extract<keyof TEnums, string>>>;
63
76
  };
64
77
  type DefineContractOptions = {
65
78
  /** @default "full" */
@@ -92,7 +105,7 @@ type DefineContractOptions = {
92
105
  * });
93
106
  * ```
94
107
  */
95
- declare function defineContract(input: DefineContractInput, options?: DefineContractOptions): Contract<IdbStorage>;
108
+ declare function defineContract<const TEnums extends EnumDefs = Record<never, never>>(input: DefineContractInput<TEnums>, options?: DefineContractOptions): Contract<IdbStorage>;
96
109
  //#endregion
97
- export { type ContractProjection, type DefineContractInput, type DefineContractOptions, type FieldSpec, type IndexDef, type ModelDef, type RelationDef, defineContract };
110
+ export { type ContractProjection, type DefineContractInput, type DefineContractOptions, type EnumDef, type EnumDefs, type FieldSpec, type IndexDef, type ModelDef, type RelationDef, defineContract };
98
111
  //# sourceMappingURL=contract-ts.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract-ts.d.mts","names":[],"sources":["../../src/core/contract-builder.ts"],"mappings":";;;;KAgBK;;;;;KAMO,YAAY,sBAAsB;KAgBlC;WACD;WACA;WACA;aACE;aACA;;WAEF,WAAW;WACX,WAAW;;KAGV;WACD;WACA;WACA;;KAGC;WACD;WACA;;WAEA,QAAQ,eAAe;WACvB,UAAU,eAAe;WACzB,YAAY,eAAe;;;;;;;;;;;;;WAa3B,gBAAgB;;WAEhB;;WAEA;;KAGC;;WAED;aAAmB;aAA0B;;;WAE7C;aAAmB;aAA2B;;WAC9C,QAAQ,eAAe;;KAGtB;;WAED,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoUR,eAAe,OAAO,qBAAqB,UAAU,wBAAwB,SAAS"}
1
+ {"version":3,"file":"contract-ts.d.mts","names":[],"sources":["../../src/core/contract-builder.ts"],"mappings":";;;;KAwBK;KAEA,wBAAwB,sBAAsB;;;;;KAMvC,UAAU,mCACpB,wBAAwB,cAAc,iBAAiB;;KAG7C;KAEA,WAAW,SAAS,eAAe;KAgBnC;WACD;WACA;WACA;aACE;aACA;;WAEF,WAAW;WACX,WAAW;;;;;;WAMX;;KAGC;;WAED,SAAS;WACT;WACA;;KAGC,SAAS;WACV;;WAEA,KAAK;;WAEL,QAAQ,eAAe,UAAU;WACjC,UAAU,eAAe;WACzB,YAAY,eAAe;;;;;;;;;;;;;WAa3B,gBAAgB;;WAEhB;;WAEA;;KAGC,oBAAoB,eAAe,WAAW;;WAE/C;aAAmB;aAA0B;;;WAE7C;aAAmB;aAA2B;;WAC9C,QAAQ;WACR,QAAQ,eAAe,SAAS,cAAc;;KAG7C;;WAED,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0cR,qBAAqB,eAAe,WAAW,sBAC7D,OAAO,oBAAoB,SAC3B,UAAU,wBACT,SAAS"}
@@ -1,5 +1,6 @@
1
- import { t as validateContract } from "../validate-BLnItEKs.mjs";
2
- import { i as warnDroppedRelation, n as isValidIdbKeyCodec, r as literalValueMatchesCodec } from "../psl-interpreter-C6Q4okuO.mjs";
1
+ import { t as validateContract } from "../validate-dwKE5_5B.mjs";
2
+ import { i as warnDroppedRelation, n as isValidIdbKeyCodec, r as literalValueMatchesCodec } from "../psl-interpreter-BoPLi4c4.mjs";
3
+ import { keyPathFields } from "@prisma-idb/target-idb/pack";
3
4
  import { UNBOUND_DOMAIN_NAMESPACE_ID, crossRef } from "@prisma/orm-framework/contract/types";
4
5
  import { computeProfileHash, computeStorageHash } from "@prisma/orm-framework/contract/hashing";
5
6
  //#region src/core/contract-builder.ts
@@ -28,12 +29,17 @@ function projectModelsForClient(models) {
28
29
  for (const [modelName, def] of Object.entries(models)) {
29
30
  if (excludedModelNames.has(modelName)) continue;
30
31
  const excludedFields = new Set(def.excludeFields ?? []);
31
- if (excludedFields.has(def.key)) throw new Error(`defineContract: model "${modelName}" excludes its own key field "${def.key}" — the client contract needs a primary key for every included model.`);
32
+ const keyFields = keyPathFields(def.key);
33
+ const excludedKeyField = keyFields.find((f) => excludedFields.has(f));
34
+ if (excludedKeyField !== void 0) throw new Error(`defineContract: model "${modelName}" excludes its own key field "${excludedKeyField}" — the client contract needs a primary key for every included model.`);
32
35
  for (const excludedField of excludedFields) {
33
- if (excludedField === def.key) continue;
36
+ if (keyFields.includes(excludedField)) continue;
34
37
  if (!(excludedField in def.fields)) throw new Error(`defineContract: model "${modelName}" excludeFields references unknown field "${excludedField}" — it is not declared in "fields".`);
35
38
  }
36
- for (const [indexName, idx] of Object.entries(def.indexes ?? {})) if (excludedFields.has(idx.keyPath)) throw new Error(`defineContract: model "${modelName}" index "${indexName}" references excluded field "${idx.keyPath}". Remove the index or the exclusion.`);
39
+ for (const [indexName, idx] of Object.entries(def.indexes ?? {})) {
40
+ const excludedIndexField = keyPathFields(idx.keyPath).find((f) => excludedFields.has(f));
41
+ if (excludedIndexField !== void 0) throw new Error(`defineContract: model "${modelName}" index "${indexName}" references excluded field "${excludedIndexField}". Remove the index or the exclusion.`);
42
+ }
37
43
  const relations = {};
38
44
  for (const [relName, rel] of Object.entries(def.relations ?? {})) {
39
45
  if (excludedModelNames.has(rel.to)) {
@@ -60,11 +66,21 @@ function projectModelsForClient(models) {
60
66
  }
61
67
  return result;
62
68
  }
63
- function resolveFieldCodecId(def, fieldName) {
69
+ function parseFieldSpec(spec) {
70
+ const nullable = spec.endsWith("?");
71
+ const withoutOptional = nullable ? spec.slice(0, -1) : spec;
72
+ const many = withoutOptional.endsWith("[]");
73
+ return {
74
+ typeName: many ? withoutOptional.slice(0, -2) : withoutOptional,
75
+ nullable,
76
+ many
77
+ };
78
+ }
79
+ function resolveFieldCodecId(def, fieldName, enums) {
64
80
  const spec = def.fields[fieldName];
65
81
  if (spec === void 0) return void 0;
66
- const typeName = spec.endsWith("?") ? spec.slice(0, -1) : spec;
67
- return SCALAR_TO_CODEC_ID[typeName];
82
+ const { typeName } = parseFieldSpec(spec);
83
+ return typeName in enums ? SCALAR_TO_CODEC_ID.String : SCALAR_TO_CODEC_ID[typeName];
68
84
  }
69
85
  /**
70
86
  * Mirrors `psl-interpreter.ts`'s `IDB_INVALID_KEY_TYPE`/`IDB_INVALID_INDEX_KEY_TYPE`
@@ -76,22 +92,42 @@ function resolveFieldCodecId(def, fieldName) {
76
92
  * (a `multiEntry` index only makes sense over a `Json`-typed field holding an
77
93
  * array — the one case this file can't statically validate either way).
78
94
  */
79
- function validateModelKeyAndIndexes(modelName, def) {
80
- if (!(def.key in def.fields)) throw new Error(`defineContract: model "${modelName}" key field "${def.key}" is not declared in "fields".`);
81
- if (def.fields[def.key]?.endsWith("?")) throw new Error(`defineContract: model "${modelName}" key field "${def.key}" is nullable ("${def.fields[def.key]}") — the primary key cannot be nullable.`);
82
- const keyCodec = resolveFieldCodecId(def, def.key);
83
- if (keyCodec !== void 0 && !isValidIdbKeyCodec(keyCodec)) throw new Error(`defineContract: model "${modelName}" key field "${def.key}" has type "${keyCodec}", which IndexedDB cannot use as a key. Every write would throw (DataError extracting the primary key). Use String, Int, Float, DateTime, Decimal, or Bytes instead.`);
95
+ function validateModelKeyAndIndexes(modelName, def, enums) {
96
+ const keyFields = keyPathFields(def.key);
97
+ if (keyFields.length === 0) throw new Error(`defineContract: model "${modelName}" "key" must name at least one field.`);
98
+ if (new Set(keyFields).size !== keyFields.length) throw new Error(`defineContract: model "${modelName}" "key" [${keyFields.join(", ")}] repeats a field name.`);
99
+ for (const keyField of keyFields) {
100
+ if (!(keyField in def.fields)) throw new Error(`defineContract: model "${modelName}" key field "${keyField}" is not declared in "fields".`);
101
+ if (def.fields[keyField] !== void 0 && parseFieldSpec(def.fields[keyField]).nullable) throw new Error(`defineContract: model "${modelName}" key field "${keyField}" is nullable ("${def.fields[keyField]}") — the primary key cannot be nullable.`);
102
+ const keyCodec = resolveFieldCodecId(def, keyField, enums);
103
+ if (keyCodec !== void 0 && !isValidIdbKeyCodec(keyCodec)) throw new Error(`defineContract: model "${modelName}" key field "${keyField}" has type "${keyCodec}", which IndexedDB cannot use as a key. Every write would throw (DataError extracting the primary key). Use String, Int, Float, DateTime, Decimal, or Bytes instead.`);
104
+ }
84
105
  for (const [indexName, idx] of Object.entries(def.indexes ?? {})) {
85
- if (!(idx.keyPath in def.fields)) throw new Error(`defineContract: model "${modelName}" index "${indexName}" references field "${idx.keyPath}", which is not declared in "fields".`);
86
- if (idx.multiEntry) continue;
87
- const fieldCodec = resolveFieldCodecId(def, idx.keyPath);
88
- if (fieldCodec === void 0 || isValidIdbKeyCodec(fieldCodec)) continue;
89
- throw new Error(`defineContract: model "${modelName}" index "${indexName}" is keyed on "${idx.keyPath}" (type "${fieldCodec}"), which IndexedDB cannot use as an index key. Records are silently omitted from the index on write, and any query against it throws at runtime. Use String, Int, Float, DateTime, Decimal, or Bytes instead.`);
106
+ const indexFields = keyPathFields(idx.keyPath);
107
+ if (indexFields.length === 0) throw new Error(`defineContract: model "${modelName}" index "${indexName}" "keyPath" must name at least one field.`);
108
+ if (new Set(indexFields).size !== indexFields.length) throw new Error(`defineContract: model "${modelName}" index "${indexName}" "keyPath" [${indexFields.join(", ")}] repeats a field name.`);
109
+ if (idx.multiEntry && indexFields.length > 1) throw new Error(`defineContract: model "${modelName}" index "${indexName}" combines "multiEntry: true" with a compound "keyPath" [${indexFields.join(", ")}] — IndexedDB rejects this combination (InvalidAccessError). multiEntry only applies to a single array-valued field.`);
110
+ for (const indexField of indexFields) {
111
+ if (!(indexField in def.fields)) throw new Error(`defineContract: model "${modelName}" index "${indexName}" references field "${indexField}", which is not declared in "fields".`);
112
+ if (idx.multiEntry) continue;
113
+ const fieldCodec = resolveFieldCodecId(def, indexField, enums);
114
+ if (fieldCodec === void 0 || isValidIdbKeyCodec(fieldCodec)) continue;
115
+ throw new Error(`defineContract: model "${modelName}" index "${indexName}" is keyed on "${indexField}" (type "${fieldCodec}"), which IndexedDB cannot use as an index key. Records are silently omitted from the index on write, and any query against it throws at runtime. Use String, Int, Float, DateTime, Decimal, or Bytes instead.`);
116
+ }
90
117
  }
91
118
  for (const [fieldName, value] of Object.entries(def.fieldDefaults ?? {})) {
92
119
  if (!(fieldName in def.fields)) throw new Error(`defineContract: model "${modelName}" fieldDefaults references field "${fieldName}", which is not declared in "fields".`);
93
- const codec = resolveFieldCodecId(def, fieldName);
120
+ const codec = resolveFieldCodecId(def, fieldName, enums);
94
121
  if (codec !== void 0 && !literalValueMatchesCodec(value, codec)) throw new Error(`defineContract: model "${modelName}" fieldDefaults["${fieldName}"] is a ${typeof value} value, but field "${fieldName}" has type "${def.fields[fieldName]}" — the literal default's JS type must match the field's declared type.`);
122
+ const spec = def.fields[fieldName];
123
+ if (spec !== void 0) {
124
+ const parsedSpec = parseFieldSpec(spec);
125
+ const enumValues = enums[parsedSpec.typeName];
126
+ if (enumValues !== void 0) {
127
+ if (parsedSpec.many) throw new Error(`defineContract: model "${modelName}" fieldDefaults["${fieldName}"] is an enum list; list defaults are not supported by IDB.`);
128
+ if (typeof value !== "string" || !enumValues.includes(value)) throw new Error(`defineContract: model "${modelName}" fieldDefaults["${fieldName}"] is not a declared value of enum "${parsedSpec.typeName}".`);
129
+ }
130
+ }
95
131
  }
96
132
  }
97
133
  function sameFieldList(a, b) {
@@ -122,23 +158,57 @@ function validateNoConflictingRelationActions(models) {
122
158
  }
123
159
  }
124
160
  }
125
- function buildFields(fields) {
161
+ function buildFields(fields, enums, namespaceId) {
126
162
  const result = {};
127
163
  for (const [name, spec] of Object.entries(fields)) {
128
- const nullable = spec.endsWith("?");
129
- const typeName = nullable ? spec.slice(0, -1) : spec;
130
- const codecId = SCALAR_TO_CODEC_ID[typeName];
164
+ const { typeName, nullable, many } = parseFieldSpec(spec);
165
+ const enumValues = enums[typeName];
166
+ const codecId = enumValues === void 0 ? SCALAR_TO_CODEC_ID[typeName] : SCALAR_TO_CODEC_ID.String;
131
167
  if (codecId === void 0) throw new Error(`Unknown field type "${typeName}" for field "${name}"`);
168
+ if (many && enumValues === void 0) throw new Error(`Field "${name}" has list type "${spec}". Only enum fields can be lists.`);
169
+ if (many && nullable) throw new Error(`Field "${name}" has type "${spec}". A list field cannot be optional.`);
132
170
  result[name] = {
133
171
  nullable,
134
172
  type: {
135
173
  kind: "scalar",
136
174
  codecId
137
- }
175
+ },
176
+ ...many ? { many: true } : {},
177
+ ...enumValues !== void 0 ? { valueSet: {
178
+ plane: "domain",
179
+ entityKind: "enum",
180
+ namespaceId,
181
+ entityName: typeName
182
+ } } : {}
138
183
  };
139
184
  }
140
185
  return result;
141
186
  }
187
+ function buildEnums(enums) {
188
+ const result = {};
189
+ for (const [name, values] of Object.entries(enums)) {
190
+ if (name in SCALAR_TO_CODEC_ID) throw new Error(`defineContract: enum "${name}" conflicts with the built-in scalar type of the same name.`);
191
+ if (values.length === 0) throw new Error(`defineContract: enum "${name}" must declare at least one value.`);
192
+ if (new Set(values).size !== values.length) throw new Error(`defineContract: enum "${name}" repeats a value. Enum values must be unique.`);
193
+ result[name] = {
194
+ codecId: SCALAR_TO_CODEC_ID.String,
195
+ members: values.map((value) => ({
196
+ name: value,
197
+ value
198
+ }))
199
+ };
200
+ }
201
+ return result;
202
+ }
203
+ /**
204
+ * Client projection: drop enums that no remaining field uses, so an enum that
205
+ * only excluded models or fields reference doesn't reach the client contract.
206
+ * Runs after `buildEnums` has validated the complete map.
207
+ */
208
+ function keepReferencedEnums(enums, models) {
209
+ const referenced = new Set(Object.values(models).flatMap((def) => Object.values(def.fields).map((spec) => parseFieldSpec(spec).typeName)));
210
+ return Object.fromEntries(Object.entries(enums).filter(([name]) => referenced.has(name)));
211
+ }
142
212
  function buildRoots(models) {
143
213
  const roots = {};
144
214
  for (const modelName of Object.keys(models)) {
@@ -163,7 +233,7 @@ function buildStores(models) {
163
233
  }
164
234
  return stores;
165
235
  }
166
- function buildModels(models) {
236
+ function buildModels(models, enums, namespaceId) {
167
237
  const result = {};
168
238
  for (const [modelName, def] of Object.entries(models)) {
169
239
  const relations = {};
@@ -175,7 +245,8 @@ function buildModels(models) {
175
245
  on: {
176
246
  localFields: rel.on.local,
177
247
  targetFields: rel.on.target
178
- }
248
+ },
249
+ ...rel.cardinality === "1:N" ? {} : { nullable: rel.nullable ?? rel.on.local.some((f) => def.fields[f]?.endsWith("?") === true) }
179
250
  };
180
251
  if (rel.onDelete !== void 0 || rel.onUpdate !== void 0) relationsStorage[relName] = {
181
252
  ...rel.onDelete !== void 0 ? { onDelete: rel.onDelete } : {},
@@ -189,7 +260,7 @@ function buildModels(models) {
189
260
  ...def.fieldDefaults && Object.keys(def.fieldDefaults).length > 0 ? { fieldDefaults: def.fieldDefaults } : {}
190
261
  };
191
262
  result[modelName] = {
192
- fields: buildFields(def.fields),
263
+ fields: buildFields(def.fields, enums, namespaceId),
193
264
  relations,
194
265
  storage
195
266
  };
@@ -224,8 +295,12 @@ function buildModels(models) {
224
295
  * ```
225
296
  */
226
297
  function defineContract(input, options) {
227
- const models = (options?.projection ?? "full") === "client" ? projectModelsForClient(input.models) : input.models;
228
- for (const [modelName, def] of Object.entries(models)) validateModelKeyAndIndexes(modelName, def);
298
+ const projection = options?.projection ?? "full";
299
+ const enums = input.enums ?? {};
300
+ const models = projection === "client" ? projectModelsForClient(input.models) : input.models;
301
+ const validatedEnums = buildEnums(enums);
302
+ const domainEnums = projection === "client" ? keepReferencedEnums(validatedEnums, models) : validatedEnums;
303
+ for (const [modelName, def] of Object.entries(models)) validateModelKeyAndIndexes(modelName, def, enums);
229
304
  validateNoConflictingRelationActions(models);
230
305
  const stores = buildStores(models);
231
306
  const capabilities = { idb: {
@@ -254,7 +329,10 @@ function defineContract(input, options) {
254
329
  ...storageBlock,
255
330
  storageHash
256
331
  };
257
- const domain = { namespaces: { [ns]: { models: buildModels(models) } } };
332
+ const domain = { namespaces: { [ns]: {
333
+ models: buildModels(models, enums, ns),
334
+ ...Object.keys(domainEnums).length > 0 ? { enum: domainEnums } : {}
335
+ } } };
258
336
  const contract = {
259
337
  target: "idb",
260
338
  targetFamily: "idb",