@prisma-idb/family-idb 0.7.0 → 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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as runIdbCli } from "../cli-Hf4zwIew.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
  }
@@ -265,7 +255,7 @@ const idbCommandFamily = defineCommandFamily({
265
255
  });
266
256
  //#endregion
267
257
  //#region package.json
268
- var version = "0.7.0";
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-Hf4zwIew.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-Hf4zwIew.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 +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-BBwZIGC3.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"}
@@ -3,11 +3,15 @@ import { IdbKeyPath, IdbReferentialAction, IdbStorage } from "@prisma-idb/target
3
3
  import { Contract } from "@prisma/orm-framework/contract/types";
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,6 +21,12 @@ 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
32
  /** A single field name, or an ordered list of field names for a compound (multi-field) index. */
@@ -24,12 +34,12 @@ type IndexDef = {
24
34
  readonly unique?: boolean;
25
35
  readonly multiEntry?: boolean;
26
36
  };
27
- type ModelDef = {
37
+ type ModelDef<EnumName extends string = never> = {
28
38
  readonly store: string;
29
39
  /** The primary key field, or an ordered list of fields for a compound primary key. */
30
40
  readonly key: IdbKeyPath;
31
41
  /** All scalar fields on the model. Use `"Type"` for non-nullable, `"Type?"` for nullable. */
32
- readonly fields: Record<string, FieldSpec>;
42
+ readonly fields: Record<string, FieldSpec<EnumName>>;
33
43
  readonly indexes?: Record<string, IndexDef>;
34
44
  readonly relations?: Record<string, RelationDef>;
35
45
  /**
@@ -50,7 +60,7 @@ type ModelDef = {
50
60
  /** Server-only fields on an otherwise-synced model — dropped in client projection. See ADR 012. */
51
61
  readonly excludeFields?: readonly string[];
52
62
  };
53
- type DefineContractInput = {
63
+ type DefineContractInput<TEnums extends EnumDefs = Record<never, never>> = {
54
64
  /** Pass the default export of `@prisma-idb/family-idb/pack`. */
55
65
  readonly family: {
56
66
  readonly familyId: "idb";
@@ -61,7 +71,8 @@ type DefineContractInput = {
61
71
  readonly targetId: string;
62
72
  readonly id: string;
63
73
  };
64
- readonly models: Record<string, ModelDef>;
74
+ readonly enums?: TEnums;
75
+ readonly models: Record<string, ModelDef<Extract<keyof TEnums, string>>>;
65
76
  };
66
77
  type DefineContractOptions = {
67
78
  /** @default "full" */
@@ -94,7 +105,7 @@ type DefineContractOptions = {
94
105
  * });
95
106
  * ```
96
107
  */
97
- 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>;
98
109
  //#endregion
99
- 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 };
100
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":";;;;KAkBK;;;;;KAMO,YAAY,sBAAsB;KAgBlC;WACD;WACA;WACA;aACE;aACA;;WAEF,WAAW;WACX,WAAW;;KAGV;;WAED,SAAS;WACT;WACA;;KAGC;WACD;;WAEA,KAAK;;WAEL,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmWR,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,5 @@
1
1
  import { t as validateContract } from "../validate-dwKE5_5B.mjs";
2
- import { i as warnDroppedRelation, n as isValidIdbKeyCodec, r as literalValueMatchesCodec } from "../psl-interpreter-BBwZIGC3.mjs";
2
+ import { i as warnDroppedRelation, n as isValidIdbKeyCodec, r as literalValueMatchesCodec } from "../psl-interpreter-BoPLi4c4.mjs";
3
3
  import { keyPathFields } from "@prisma-idb/target-idb/pack";
4
4
  import { UNBOUND_DOMAIN_NAMESPACE_ID, crossRef } from "@prisma/orm-framework/contract/types";
5
5
  import { computeProfileHash, computeStorageHash } from "@prisma/orm-framework/contract/hashing";
@@ -66,11 +66,21 @@ function projectModelsForClient(models) {
66
66
  }
67
67
  return result;
68
68
  }
69
- 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) {
70
80
  const spec = def.fields[fieldName];
71
81
  if (spec === void 0) return void 0;
72
- const typeName = spec.endsWith("?") ? spec.slice(0, -1) : spec;
73
- 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];
74
84
  }
75
85
  /**
76
86
  * Mirrors `psl-interpreter.ts`'s `IDB_INVALID_KEY_TYPE`/`IDB_INVALID_INDEX_KEY_TYPE`
@@ -82,14 +92,14 @@ function resolveFieldCodecId(def, fieldName) {
82
92
  * (a `multiEntry` index only makes sense over a `Json`-typed field holding an
83
93
  * array — the one case this file can't statically validate either way).
84
94
  */
85
- function validateModelKeyAndIndexes(modelName, def) {
95
+ function validateModelKeyAndIndexes(modelName, def, enums) {
86
96
  const keyFields = keyPathFields(def.key);
87
97
  if (keyFields.length === 0) throw new Error(`defineContract: model "${modelName}" "key" must name at least one field.`);
88
98
  if (new Set(keyFields).size !== keyFields.length) throw new Error(`defineContract: model "${modelName}" "key" [${keyFields.join(", ")}] repeats a field name.`);
89
99
  for (const keyField of keyFields) {
90
100
  if (!(keyField in def.fields)) throw new Error(`defineContract: model "${modelName}" key field "${keyField}" is not declared in "fields".`);
91
- if (def.fields[keyField]?.endsWith("?")) throw new Error(`defineContract: model "${modelName}" key field "${keyField}" is nullable ("${def.fields[keyField]}") — the primary key cannot be nullable.`);
92
- const keyCodec = resolveFieldCodecId(def, keyField);
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);
93
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.`);
94
104
  }
95
105
  for (const [indexName, idx] of Object.entries(def.indexes ?? {})) {
@@ -100,15 +110,24 @@ function validateModelKeyAndIndexes(modelName, def) {
100
110
  for (const indexField of indexFields) {
101
111
  if (!(indexField in def.fields)) throw new Error(`defineContract: model "${modelName}" index "${indexName}" references field "${indexField}", which is not declared in "fields".`);
102
112
  if (idx.multiEntry) continue;
103
- const fieldCodec = resolveFieldCodecId(def, indexField);
113
+ const fieldCodec = resolveFieldCodecId(def, indexField, enums);
104
114
  if (fieldCodec === void 0 || isValidIdbKeyCodec(fieldCodec)) continue;
105
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.`);
106
116
  }
107
117
  }
108
118
  for (const [fieldName, value] of Object.entries(def.fieldDefaults ?? {})) {
109
119
  if (!(fieldName in def.fields)) throw new Error(`defineContract: model "${modelName}" fieldDefaults references field "${fieldName}", which is not declared in "fields".`);
110
- const codec = resolveFieldCodecId(def, fieldName);
120
+ const codec = resolveFieldCodecId(def, fieldName, enums);
111
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
+ }
112
131
  }
113
132
  }
114
133
  function sameFieldList(a, b) {
@@ -139,23 +158,57 @@ function validateNoConflictingRelationActions(models) {
139
158
  }
140
159
  }
141
160
  }
142
- function buildFields(fields) {
161
+ function buildFields(fields, enums, namespaceId) {
143
162
  const result = {};
144
163
  for (const [name, spec] of Object.entries(fields)) {
145
- const nullable = spec.endsWith("?");
146
- const typeName = nullable ? spec.slice(0, -1) : spec;
147
- 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;
148
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.`);
149
170
  result[name] = {
150
171
  nullable,
151
172
  type: {
152
173
  kind: "scalar",
153
174
  codecId
154
- }
175
+ },
176
+ ...many ? { many: true } : {},
177
+ ...enumValues !== void 0 ? { valueSet: {
178
+ plane: "domain",
179
+ entityKind: "enum",
180
+ namespaceId,
181
+ entityName: typeName
182
+ } } : {}
183
+ };
184
+ }
185
+ return result;
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
+ }))
155
199
  };
156
200
  }
157
201
  return result;
158
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
+ }
159
212
  function buildRoots(models) {
160
213
  const roots = {};
161
214
  for (const modelName of Object.keys(models)) {
@@ -180,7 +233,7 @@ function buildStores(models) {
180
233
  }
181
234
  return stores;
182
235
  }
183
- function buildModels(models) {
236
+ function buildModels(models, enums, namespaceId) {
184
237
  const result = {};
185
238
  for (const [modelName, def] of Object.entries(models)) {
186
239
  const relations = {};
@@ -192,7 +245,8 @@ function buildModels(models) {
192
245
  on: {
193
246
  localFields: rel.on.local,
194
247
  targetFields: rel.on.target
195
- }
248
+ },
249
+ ...rel.cardinality === "1:N" ? {} : { nullable: rel.nullable ?? rel.on.local.some((f) => def.fields[f]?.endsWith("?") === true) }
196
250
  };
197
251
  if (rel.onDelete !== void 0 || rel.onUpdate !== void 0) relationsStorage[relName] = {
198
252
  ...rel.onDelete !== void 0 ? { onDelete: rel.onDelete } : {},
@@ -206,7 +260,7 @@ function buildModels(models) {
206
260
  ...def.fieldDefaults && Object.keys(def.fieldDefaults).length > 0 ? { fieldDefaults: def.fieldDefaults } : {}
207
261
  };
208
262
  result[modelName] = {
209
- fields: buildFields(def.fields),
263
+ fields: buildFields(def.fields, enums, namespaceId),
210
264
  relations,
211
265
  storage
212
266
  };
@@ -241,8 +295,12 @@ function buildModels(models) {
241
295
  * ```
242
296
  */
243
297
  function defineContract(input, options) {
244
- const models = (options?.projection ?? "full") === "client" ? projectModelsForClient(input.models) : input.models;
245
- 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);
246
304
  validateNoConflictingRelationActions(models);
247
305
  const stores = buildStores(models);
248
306
  const capabilities = { idb: {
@@ -271,7 +329,10 @@ function defineContract(input, options) {
271
329
  ...storageBlock,
272
330
  storageHash
273
331
  };
274
- 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
+ } } };
275
336
  const contract = {
276
337
  target: "idb",
277
338
  targetFamily: "idb",