@prisma-next/migration-tools 0.4.0-dev.9 → 0.4.2

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 (56) hide show
  1. package/README.md +1 -1
  2. package/dist/{attestation-DnebS4XZ.mjs → attestation-BnzTb0Qp.mjs} +24 -23
  3. package/dist/attestation-BnzTb0Qp.mjs.map +1 -0
  4. package/dist/{errors-C_XuSbX7.mjs → errors-BmiSgz1j.mjs} +14 -7
  5. package/dist/errors-BmiSgz1j.mjs.map +1 -0
  6. package/dist/exports/attestation.d.mts +20 -6
  7. package/dist/exports/attestation.d.mts.map +1 -1
  8. package/dist/exports/attestation.mjs +3 -3
  9. package/dist/exports/dag.d.mts +8 -6
  10. package/dist/exports/dag.d.mts.map +1 -1
  11. package/dist/exports/dag.mjs +181 -107
  12. package/dist/exports/dag.mjs.map +1 -1
  13. package/dist/exports/io.d.mts +16 -13
  14. package/dist/exports/io.d.mts.map +1 -1
  15. package/dist/exports/io.mjs +2 -2
  16. package/dist/exports/migration-ts.d.mts +15 -21
  17. package/dist/exports/migration-ts.d.mts.map +1 -1
  18. package/dist/exports/migration-ts.mjs +28 -36
  19. package/dist/exports/migration-ts.mjs.map +1 -1
  20. package/dist/exports/migration.d.mts +48 -18
  21. package/dist/exports/migration.d.mts.map +1 -1
  22. package/dist/exports/migration.mjs +75 -85
  23. package/dist/exports/migration.mjs.map +1 -1
  24. package/dist/exports/refs.d.mts +11 -5
  25. package/dist/exports/refs.d.mts.map +1 -1
  26. package/dist/exports/refs.mjs +106 -30
  27. package/dist/exports/refs.mjs.map +1 -1
  28. package/dist/exports/types.d.mts +2 -2
  29. package/dist/exports/types.mjs +2 -16
  30. package/dist/{io-Cun81AIZ.mjs → io-Cd6GLyjK.mjs} +18 -22
  31. package/dist/io-Cd6GLyjK.mjs.map +1 -0
  32. package/dist/types-DyGXcWWp.d.mts +71 -0
  33. package/dist/types-DyGXcWWp.d.mts.map +1 -0
  34. package/package.json +5 -4
  35. package/src/attestation.ts +34 -26
  36. package/src/dag.ts +140 -154
  37. package/src/errors.ts +8 -0
  38. package/src/exports/attestation.ts +2 -1
  39. package/src/exports/io.ts +1 -1
  40. package/src/exports/migration-ts.ts +1 -1
  41. package/src/exports/migration.ts +8 -1
  42. package/src/exports/refs.ts +10 -2
  43. package/src/exports/types.ts +2 -8
  44. package/src/graph-ops.ts +65 -0
  45. package/src/io.ts +23 -24
  46. package/src/migration-base.ts +99 -101
  47. package/src/migration-ts.ts +28 -50
  48. package/src/queue.ts +37 -0
  49. package/src/refs.ts +148 -37
  50. package/src/types.ts +15 -55
  51. package/dist/attestation-DnebS4XZ.mjs.map +0 -1
  52. package/dist/errors-C_XuSbX7.mjs.map +0 -1
  53. package/dist/exports/types.mjs.map +0 -1
  54. package/dist/io-Cun81AIZ.mjs.map +0 -1
  55. package/dist/types-D2uX4ql7.d.mts +0 -100
  56. package/dist/types-D2uX4ql7.d.mts.map +0 -1
@@ -1,28 +1,44 @@
1
1
  import { stat, writeFile } from "node:fs/promises";
2
- import { join, resolve } from "pathe";
2
+ import { join } from "pathe";
3
+ import { format } from "prettier";
3
4
 
4
5
  //#region src/migration-ts.ts
5
6
  /**
6
7
  * Utilities for reading/writing `migration.ts` files.
7
8
  *
8
- * Rendering migration.ts source is now the target's responsibility — the CLI
9
- * obtains source strings either from a class-flow planner's
10
- * `plan.renderTypeScript()` or from a descriptor-flow target's
11
- * `migrations.renderDescriptorTypeScript(descriptors, context)`. The helper
12
- * here is limited to file I/O: writing the returned source with the right
13
- * executable bit, probing for existence, and evaluating legacy descriptor-
14
- * flow files.
9
+ * Rendering migration.ts source is the target's responsibility — the CLI
10
+ * obtains source strings from a planner's `plan.renderTypeScript()`. The
11
+ * helper here is limited to file I/O: writing the returned source with the
12
+ * right executable bit and probing for existence.
15
13
  */
16
14
  const MIGRATION_TS_FILE = "migration.ts";
17
15
  /**
18
16
  * Writes a pre-rendered `migration.ts` source string to the given package
19
17
  * directory. If the source begins with a shebang, the file is written with
20
18
  * executable permissions (0o755) so it can be run directly via
21
- * `./migration.ts` by the authoring class's `Migration.run(...)` guard.
19
+ * `./migration.ts` the rendered scaffold ends with
20
+ * `MigrationCLI.run(import.meta.url, M)` from
21
+ * `@prisma-next/cli/migration-cli` (re-exported by the postgres facade),
22
+ * which guards on the entrypoint and serializes when the file is the main
23
+ * module.
24
+ *
25
+ * The source is run through prettier before writing so migration renderers
26
+ * can produce structurally-correct but loosely-indented source and rely on
27
+ * a single canonical format on disk. Matches what `@prisma-next/emitter`
28
+ * already does for generated `contract.d.ts`.
22
29
  */
23
30
  async function writeMigrationTs(packageDir, content) {
24
- const isExecutable = content.startsWith("#!");
25
- await writeFile(join(packageDir, MIGRATION_TS_FILE), content, isExecutable ? { mode: 493 } : void 0);
31
+ const formatted = await formatMigrationTsSource(content);
32
+ const isExecutable = formatted.startsWith("#!");
33
+ await writeFile(join(packageDir, MIGRATION_TS_FILE), formatted, isExecutable ? { mode: 493 } : void 0);
34
+ }
35
+ async function formatMigrationTsSource(source) {
36
+ return format(source, {
37
+ parser: "typescript",
38
+ singleQuote: true,
39
+ semi: true,
40
+ printWidth: 100
41
+ });
26
42
  }
27
43
  /**
28
44
  * Checks whether a migration.ts file exists in the package directory.
@@ -34,30 +50,6 @@ async function hasMigrationTs(packageDir) {
34
50
  return false;
35
51
  }
36
52
  }
37
- /**
38
- * Evaluates a descriptor-flow migration.ts file by loading it via native
39
- * Node import. Returns the result of calling the default export (expected
40
- * to be a function returning an array of operation descriptors).
41
- *
42
- * Class-flow migration.ts files use a different shape — their default
43
- * export is a class that extends `Migration` — and are evaluated by the
44
- * target's `emit` capability, not this helper.
45
- *
46
- * Requires Node ≥24 for native TypeScript support.
47
- */
48
- async function evaluateMigrationTs(packageDir) {
49
- const filePath = resolve(join(packageDir, MIGRATION_TS_FILE));
50
- try {
51
- await stat(filePath);
52
- } catch {
53
- throw new Error(`migration.ts not found at "${filePath}"`);
54
- }
55
- const mod = await import(filePath);
56
- if (typeof mod.default !== "function") throw new Error(`migration.ts must export a default function returning an operation list. Got: ${typeof mod.default}`);
57
- const result = mod.default();
58
- if (!Array.isArray(result)) throw new Error(`migration.ts default export must return an array of operations. Got: ${typeof result}`);
59
- return result;
60
- }
61
53
 
62
54
  //#endregion
63
55
  //#region src/runtime-detection.ts
@@ -75,5 +67,5 @@ function shebangLineFor(runtime) {
75
67
  }
76
68
 
77
69
  //#endregion
78
- export { detectScaffoldRuntime, evaluateMigrationTs, hasMigrationTs, shebangLineFor, writeMigrationTs };
70
+ export { detectScaffoldRuntime, hasMigrationTs, shebangLineFor, writeMigrationTs };
79
71
  //# sourceMappingURL=migration-ts.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"migration-ts.mjs","names":["result: unknown"],"sources":["../../src/migration-ts.ts","../../src/runtime-detection.ts"],"sourcesContent":["/**\n * Utilities for reading/writing `migration.ts` files.\n *\n * Rendering migration.ts source is now the target's responsibility — the CLI\n * obtains source strings either from a class-flow planner's\n * `plan.renderTypeScript()` or from a descriptor-flow target's\n * `migrations.renderDescriptorTypeScript(descriptors, context)`. The helper\n * here is limited to file I/O: writing the returned source with the right\n * executable bit, probing for existence, and evaluating legacy descriptor-\n * flow files.\n */\n\nimport { stat, writeFile } from 'node:fs/promises';\nimport { join, resolve } from 'pathe';\n\nconst MIGRATION_TS_FILE = 'migration.ts';\n\n/**\n * Writes a pre-rendered `migration.ts` source string to the given package\n * directory. If the source begins with a shebang, the file is written with\n * executable permissions (0o755) so it can be run directly via\n * `./migration.ts` by the authoring class's `Migration.run(...)` guard.\n */\nexport async function writeMigrationTs(packageDir: string, content: string): Promise<void> {\n const isExecutable = content.startsWith('#!');\n await writeFile(\n join(packageDir, MIGRATION_TS_FILE),\n content,\n isExecutable ? { mode: 0o755 } : undefined,\n );\n}\n\n/**\n * Checks whether a migration.ts file exists in the package directory.\n */\nexport async function hasMigrationTs(packageDir: string): Promise<boolean> {\n try {\n const s = await stat(join(packageDir, MIGRATION_TS_FILE));\n return s.isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Evaluates a descriptor-flow migration.ts file by loading it via native\n * Node import. Returns the result of calling the default export (expected\n * to be a function returning an array of operation descriptors).\n *\n * Class-flow migration.ts files use a different shape — their default\n * export is a class that extends `Migration` — and are evaluated by the\n * target's `emit` capability, not this helper.\n *\n * Requires Node ≥24 for native TypeScript support.\n */\nexport async function evaluateMigrationTs(packageDir: string): Promise<readonly unknown[]> {\n const filePath = resolve(join(packageDir, MIGRATION_TS_FILE));\n\n try {\n await stat(filePath);\n } catch {\n throw new Error(`migration.ts not found at \"${filePath}\"`);\n }\n\n const mod = (await import(filePath)) as { default?: unknown };\n\n if (typeof mod.default !== 'function') {\n throw new Error(\n `migration.ts must export a default function returning an operation list. Got: ${typeof mod.default}`,\n );\n }\n\n const result: unknown = mod.default();\n\n if (!Array.isArray(result)) {\n throw new Error(\n `migration.ts default export must return an array of operations. Got: ${typeof result}`,\n );\n }\n\n return result;\n}\n","export type ScaffoldRuntime = 'node' | 'bun' | 'deno';\n\nexport function detectScaffoldRuntime(): ScaffoldRuntime {\n if (typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined') return 'bun';\n if (typeof (globalThis as { Deno?: unknown }).Deno !== 'undefined') return 'deno';\n return 'node';\n}\n\nexport function shebangLineFor(runtime: ScaffoldRuntime): string {\n switch (runtime) {\n case 'bun':\n return '#!/usr/bin/env -S bun';\n case 'deno':\n return '#!/usr/bin/env -S deno run -A';\n case 'node':\n return '#!/usr/bin/env -S node';\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAM,oBAAoB;;;;;;;AAQ1B,eAAsB,iBAAiB,YAAoB,SAAgC;CACzF,MAAM,eAAe,QAAQ,WAAW,KAAK;AAC7C,OAAM,UACJ,KAAK,YAAY,kBAAkB,EACnC,SACA,eAAe,EAAE,MAAM,KAAO,GAAG,OAClC;;;;;AAMH,eAAsB,eAAe,YAAsC;AACzE,KAAI;AAEF,UADU,MAAM,KAAK,KAAK,YAAY,kBAAkB,CAAC,EAChD,QAAQ;SACX;AACN,SAAO;;;;;;;;;;;;;;AAeX,eAAsB,oBAAoB,YAAiD;CACzF,MAAM,WAAW,QAAQ,KAAK,YAAY,kBAAkB,CAAC;AAE7D,KAAI;AACF,QAAM,KAAK,SAAS;SACd;AACN,QAAM,IAAI,MAAM,8BAA8B,SAAS,GAAG;;CAG5D,MAAM,MAAO,MAAM,OAAO;AAE1B,KAAI,OAAO,IAAI,YAAY,WACzB,OAAM,IAAI,MACR,iFAAiF,OAAO,IAAI,UAC7F;CAGH,MAAMA,SAAkB,IAAI,SAAS;AAErC,KAAI,CAAC,MAAM,QAAQ,OAAO,CACxB,OAAM,IAAI,MACR,wEAAwE,OAAO,SAChF;AAGH,QAAO;;;;;AC9ET,SAAgB,wBAAyC;AACvD,KAAI,OAAQ,WAAiC,QAAQ,YAAa,QAAO;AACzE,KAAI,OAAQ,WAAkC,SAAS,YAAa,QAAO;AAC3E,QAAO;;AAGT,SAAgB,eAAe,SAAkC;AAC/D,SAAQ,SAAR;EACE,KAAK,MACH,QAAO;EACT,KAAK,OACH,QAAO;EACT,KAAK,OACH,QAAO"}
1
+ {"version":3,"file":"migration-ts.mjs","names":[],"sources":["../../src/migration-ts.ts","../../src/runtime-detection.ts"],"sourcesContent":["/**\n * Utilities for reading/writing `migration.ts` files.\n *\n * Rendering migration.ts source is the target's responsibility — the CLI\n * obtains source strings from a planner's `plan.renderTypeScript()`. The\n * helper here is limited to file I/O: writing the returned source with the\n * right executable bit and probing for existence.\n */\n\nimport { stat, writeFile } from 'node:fs/promises';\nimport { join } from 'pathe';\nimport { format } from 'prettier';\n\nconst MIGRATION_TS_FILE = 'migration.ts';\n\n/**\n * Writes a pre-rendered `migration.ts` source string to the given package\n * directory. If the source begins with a shebang, the file is written with\n * executable permissions (0o755) so it can be run directly via\n * `./migration.ts` the rendered scaffold ends with\n * `MigrationCLI.run(import.meta.url, M)` from\n * `@prisma-next/cli/migration-cli` (re-exported by the postgres facade),\n * which guards on the entrypoint and serializes when the file is the main\n * module.\n *\n * The source is run through prettier before writing so migration renderers\n * can produce structurally-correct but loosely-indented source and rely on\n * a single canonical format on disk. Matches what `@prisma-next/emitter`\n * already does for generated `contract.d.ts`.\n */\nexport async function writeMigrationTs(packageDir: string, content: string): Promise<void> {\n const formatted = await formatMigrationTsSource(content);\n const isExecutable = formatted.startsWith('#!');\n await writeFile(\n join(packageDir, MIGRATION_TS_FILE),\n formatted,\n isExecutable ? { mode: 0o755 } : undefined,\n );\n}\n\nasync function formatMigrationTsSource(source: string): Promise<string> {\n return format(source, {\n parser: 'typescript',\n singleQuote: true,\n semi: true,\n printWidth: 100,\n });\n}\n\n/**\n * Checks whether a migration.ts file exists in the package directory.\n */\nexport async function hasMigrationTs(packageDir: string): Promise<boolean> {\n try {\n const s = await stat(join(packageDir, MIGRATION_TS_FILE));\n return s.isFile();\n } catch {\n return false;\n }\n}\n","export type ScaffoldRuntime = 'node' | 'bun' | 'deno';\n\nexport function detectScaffoldRuntime(): ScaffoldRuntime {\n if (typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined') return 'bun';\n if (typeof (globalThis as { Deno?: unknown }).Deno !== 'undefined') return 'deno';\n return 'node';\n}\n\nexport function shebangLineFor(runtime: ScaffoldRuntime): string {\n switch (runtime) {\n case 'bun':\n return '#!/usr/bin/env -S bun';\n case 'deno':\n return '#!/usr/bin/env -S deno run -A';\n case 'node':\n return '#!/usr/bin/env -S node';\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAM,oBAAoB;;;;;;;;;;;;;;;;AAiB1B,eAAsB,iBAAiB,YAAoB,SAAgC;CACzF,MAAM,YAAY,MAAM,wBAAwB,QAAQ;CACxD,MAAM,eAAe,UAAU,WAAW,KAAK;AAC/C,OAAM,UACJ,KAAK,YAAY,kBAAkB,EACnC,WACA,eAAe,EAAE,MAAM,KAAO,GAAG,OAClC;;AAGH,eAAe,wBAAwB,QAAiC;AACtE,QAAO,OAAO,QAAQ;EACpB,QAAQ;EACR,aAAa;EACb,MAAM;EACN,YAAY;EACb,CAAC;;;;;AAMJ,eAAsB,eAAe,YAAsC;AACzE,KAAI;AAEF,UADU,MAAM,KAAK,KAAK,YAAY,kBAAkB,CAAC,EAChD,QAAQ;SACX;AACN,SAAO;;;;;;ACvDX,SAAgB,wBAAyC;AACvD,KAAI,OAAQ,WAAiC,QAAQ,YAAa,QAAO;AACzE,KAAI,OAAQ,WAAkC,SAAS,YAAa,QAAO;AAC3E,QAAO;;AAGT,SAAgB,eAAe,SAAkC;AAC/D,SAAQ,SAAR;EACE,KAAK,MACH,QAAO;EACT,KAAK,OACH,QAAO;EACT,KAAK,OACH,QAAO"}
@@ -1,4 +1,5 @@
1
- import { MigrationPlan, MigrationPlanOperation } from "@prisma-next/framework-components/control";
1
+ import { a as MigrationManifest } from "../types-DyGXcWWp.mjs";
2
+ import { ControlStack, MigrationPlan, MigrationPlanOperation } from "@prisma-next/framework-components/control";
2
3
 
3
4
  //#region src/migration-base.d.ts
4
5
  interface MigrationMeta {
@@ -8,7 +9,7 @@ interface MigrationMeta {
8
9
  readonly labels?: readonly string[];
9
10
  }
10
11
  /**
11
- * Base class for class-flow migrations.
12
+ * Base class for migrations.
12
13
  *
13
14
  * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
14
15
  * runner can consume it directly via `targetId`, `operations`, `origin`, and
@@ -16,8 +17,19 @@ interface MigrationMeta {
16
17
  * every migration must implement — `migration.json` is required for a
17
18
  * migration to be valid.
18
19
  */
19
- declare abstract class Migration<TOperation extends MigrationPlanOperation = MigrationPlanOperation> implements MigrationPlan {
20
+ declare abstract class Migration<TOperation extends MigrationPlanOperation = MigrationPlanOperation, TFamilyId extends string = string, TTargetId extends string = string> implements MigrationPlan {
20
21
  abstract readonly targetId: string;
22
+ /**
23
+ * Assembled `ControlStack` injected by the orchestrator (`runMigration`).
24
+ *
25
+ * Subclasses (e.g. `PostgresMigration`) read the stack to materialize their
26
+ * adapter once per instance. Optional at the abstract level so unit tests can
27
+ * construct `Migration` instances purely for `operations` / `describe`
28
+ * assertions without needing a real stack; concrete subclasses that need the
29
+ * stack at runtime should narrow the parameter to required.
30
+ */
31
+ protected readonly stack: ControlStack<TFamilyId, TTargetId> | undefined;
32
+ constructor(stack?: ControlStack<TFamilyId, TTargetId>);
21
33
  /**
22
34
  * Ordered list of operations this migration performs.
23
35
  *
@@ -37,21 +49,39 @@ declare abstract class Migration<TOperation extends MigrationPlanOperation = Mig
37
49
  get destination(): {
38
50
  readonly storageHash: string;
39
51
  };
40
- /**
41
- * Entrypoint guard for migration files. When called at module scope,
42
- * detects whether the file is being run directly (e.g. `node migration.ts`)
43
- * and if so, serializes the migration plan to `ops.json` and
44
- * `migration.json` in the same directory. When the file is imported by
45
- * another module, this is a no-op.
46
- *
47
- * Usage (at module scope, after the class definition):
48
- *
49
- * class MyMigration extends Migration { ... }
50
- * export default MyMigration;
51
- * Migration.run(import.meta.url, MyMigration);
52
- */
53
- static run(importMetaUrl: string, MigrationClass: new () => Migration): void;
54
52
  }
53
+ /**
54
+ * Returns true when `import.meta.url` resolves to the same file that was
55
+ * invoked as the node entrypoint (`process.argv[1]`). Used by
56
+ * `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when
57
+ * the migration module is being imported (e.g. by another script) rather
58
+ * than executed directly.
59
+ */
60
+ declare function isDirectEntrypoint(importMetaUrl: string): boolean;
61
+ declare function printMigrationHelp(): void;
62
+ /**
63
+ * In-memory artifacts produced from a `Migration` instance: the
64
+ * serialized `ops.json` body, the `migration.json` manifest object, and
65
+ * its serialized form. Returned by `buildMigrationArtifacts` so callers
66
+ * (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can
67
+ * decide how to persist them — write to disk, print in dry-run, ship
68
+ * over the wire — without coupling artifact construction to file I/O.
69
+ */
70
+ interface MigrationArtifacts {
71
+ readonly opsJson: string;
72
+ readonly manifest: MigrationManifest;
73
+ readonly manifestJson: string;
74
+ }
75
+ /**
76
+ * Pure conversion from a `Migration` instance (plus the previously
77
+ * scaffolded manifest, when one exists on disk) to the in-memory
78
+ * artifacts that downstream tooling persists. Owns metadata validation,
79
+ * manifest synthesis/preservation, hint normalization, and the
80
+ * content-addressed `migrationId` computation, but performs no file I/O
81
+ * — callers handle reads (to source `existing`) and writes (to persist
82
+ * `opsJson` / `manifestJson`).
83
+ */
84
+ declare function buildMigrationArtifacts(instance: Migration, existing: Partial<MigrationManifest> | null): MigrationArtifacts;
55
85
  //#endregion
56
- export { Migration, type MigrationMeta };
86
+ export { Migration, type MigrationArtifacts, type MigrationMeta, buildMigrationArtifacts, isDirectEntrypoint, printMigrationHelp };
57
87
  //# sourceMappingURL=migration.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"migration.d.mts","names":[],"sources":["../../src/migration-base.ts"],"sourcesContent":[],"mappings":";;;UAaiB,aAAA;;EAAA,SAAA,EAAA,EAAA,MAAa;EAuBR,SAAA,IAAS,CAAA,EAAA,SAAA,GAAA,UAAA;EAAoB,SAAA,MAAA,CAAA,EAAA,SAAA,MAAA,EAAA;;;;;;;;;;;uBAA7B,6BAA6B,yBAAyB,mCAC/D;;;;;;;;sCAUyB;;;;;;uBAOf;;;;;;;;;;;;;;;;;;;;8DA4BuC"}
1
+ {"version":3,"file":"migration.d.mts","names":[],"sources":["../../src/migration-base.ts"],"sourcesContent":[],"mappings":";;;;UAaiB,aAAA;;EAAA,SAAA,EAAA,EAAA,MAAa;EAuBR,SAAA,IAAS,CAAA,EAAA,SAAA,GAAA,UAAA;EACV,SAAA,MAAA,CAAA,EAAA,SAAA,MAAA,EAAA;;;;;;;;;;;AAGK,uBAJJ,SAII,CAAA,mBAHL,sBAGK,GAHoB,sBAGpB,EAAA,kBAAA,MAAA,GAAA,MAAA,EAAA,kBAAA,MAAA,GAAA,MAAA,CAAA,YAAb,aAAa,CAAA;EAuDV,kBAAA,QAAkB,EAAA,MAAA;EAWlB;AAyBhB;AAyEA;;;;;;;4BAvJ4B,aAAa,WAAW;sBAE9B,aAAa,WAAW;;;;;;;sCAUR;;;;;;uBAOf;;;;;;;;;;;;;;;iBAuBP,kBAAA;iBAWA,kBAAA,CAAA;;;;;;;;;UAyBC,kBAAA;;qBAEI;;;;;;;;;;;;iBAuEL,uBAAA,WACJ,qBACA,QAAQ,4BACjB"}
@@ -1,9 +1,8 @@
1
- import "../io-Cun81AIZ.mjs";
2
- import { n as computeMigrationId } from "../attestation-DnebS4XZ.mjs";
1
+ import "../io-Cd6GLyjK.mjs";
2
+ import { t as computeMigrationId } from "../attestation-BnzTb0Qp.mjs";
3
3
  import { type } from "arktype";
4
- import { dirname, join } from "pathe";
5
4
  import { ifDefined } from "@prisma-next/utils/defined";
6
- import { readFileSync, realpathSync, writeFileSync } from "node:fs";
5
+ import { realpathSync } from "node:fs";
7
6
  import { fileURLToPath } from "node:url";
8
7
 
9
8
  //#region src/migration-base.ts
@@ -14,7 +13,7 @@ const MigrationMetaSchema = type({
14
13
  "labels?": type("string").array()
15
14
  });
16
15
  /**
17
- * Base class for class-flow migrations.
16
+ * Base class for migrations.
18
17
  *
19
18
  * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
20
19
  * runner can consume it directly via `targetId`, `operations`, `origin`, and
@@ -23,6 +22,19 @@ const MigrationMetaSchema = type({
23
22
  * migration to be valid.
24
23
  */
25
24
  var Migration = class {
25
+ /**
26
+ * Assembled `ControlStack` injected by the orchestrator (`runMigration`).
27
+ *
28
+ * Subclasses (e.g. `PostgresMigration`) read the stack to materialize their
29
+ * adapter once per instance. Optional at the abstract level so unit tests can
30
+ * construct `Migration` instances purely for `operations` / `describe`
31
+ * assertions without needing a real stack; concrete subclasses that need the
32
+ * stack at runtime should narrow the parameter to required.
33
+ */
34
+ stack;
35
+ constructor(stack) {
36
+ this.stack = stack;
37
+ }
26
38
  get origin() {
27
39
  const from = this.describe().from;
28
40
  return from === "" ? null : { storageHash: from };
@@ -30,46 +42,27 @@ var Migration = class {
30
42
  get destination() {
31
43
  return { storageHash: this.describe().to };
32
44
  }
33
- /**
34
- * Entrypoint guard for migration files. When called at module scope,
35
- * detects whether the file is being run directly (e.g. `node migration.ts`)
36
- * and if so, serializes the migration plan to `ops.json` and
37
- * `migration.json` in the same directory. When the file is imported by
38
- * another module, this is a no-op.
39
- *
40
- * Usage (at module scope, after the class definition):
41
- *
42
- * class MyMigration extends Migration { ... }
43
- * export default MyMigration;
44
- * Migration.run(import.meta.url, MyMigration);
45
- */
46
- static run(importMetaUrl, MigrationClass) {
47
- if (!importMetaUrl) return;
48
- const metaFilename = fileURLToPath(importMetaUrl);
49
- const argv1 = process.argv[1];
50
- if (!argv1) return;
51
- let isEntrypoint;
52
- try {
53
- isEntrypoint = realpathSync(metaFilename) === realpathSync(argv1);
54
- } catch {
55
- return;
56
- }
57
- if (!isEntrypoint) return;
58
- const args = process.argv.slice(2);
59
- if (args.includes("--help")) {
60
- printHelp();
61
- return;
62
- }
63
- const dryRun = args.includes("--dry-run");
64
- const migrationDir = dirname(metaFilename);
65
- try {
66
- serializeMigration(MigrationClass, migrationDir, dryRun);
67
- } catch (err) {
68
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
69
- process.exitCode = 1;
70
- }
71
- }
72
45
  };
46
+ /**
47
+ * Returns true when `import.meta.url` resolves to the same file that was
48
+ * invoked as the node entrypoint (`process.argv[1]`). Used by
49
+ * `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when
50
+ * the migration module is being imported (e.g. by another script) rather
51
+ * than executed directly.
52
+ */
53
+ function isDirectEntrypoint(importMetaUrl) {
54
+ const metaFilename = fileURLToPath(importMetaUrl);
55
+ const argv1 = process.argv[1];
56
+ if (!argv1) return false;
57
+ try {
58
+ return realpathSync(metaFilename) === realpathSync(argv1);
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ function printMigrationHelp() {
64
+ printHelp();
65
+ }
73
66
  function printHelp() {
74
67
  process.stdout.write([
75
68
  "Usage: node <migration-file> [options]",
@@ -81,10 +74,11 @@ function printHelp() {
81
74
  ].join("\n"));
82
75
  }
83
76
  /**
84
- * Build the attested manifest written by `Migration.run()`.
77
+ * Build the attested manifest from `describe()`-derived metadata, the
78
+ * operations list, and the previously-scaffolded manifest (if any).
85
79
  *
86
- * When a `migration.json` already exists in the directory (the common case:
87
- * the package was scaffolded by `migration plan`), preserve the contract
80
+ * When a `migration.json` already exists for this package (the common
81
+ * case: it was scaffolded by `migration plan`), preserve the contract
88
82
  * bookends, hints, labels, and `createdAt` set there — those fields are
89
83
  * owned by the CLI scaffolder, not the authored class. Only the
90
84
  * `describe()`-derived fields (`from`, `to`, `kind`) and the operations
@@ -93,14 +87,11 @@ function printHelp() {
93
87
  * schema-conformant manifest so the resulting package can still be read,
94
88
  * verified, and applied.
95
89
  *
96
- * In both cases the `migrationId` is recomputed against the current
97
- * manifest + ops so the on-disk artifacts are always fully attested — no
98
- * draft (`migrationId: null`) ever leaves this function.
90
+ * The `migrationId` is recomputed against the current manifest + ops so
91
+ * the on-disk artifacts are always fully attested.
99
92
  */
100
- function buildAttestedManifest(migrationDir, meta, ops) {
101
- const existing = readExistingManifest(join(migrationDir, "migration.json"));
93
+ function buildAttestedManifest(meta, ops, existing) {
102
94
  const baseManifest = {
103
- migrationId: null,
104
95
  from: meta.from,
105
96
  to: meta.to,
106
97
  kind: meta.kind ?? "regular",
@@ -108,12 +99,7 @@ function buildAttestedManifest(migrationDir, meta, ops) {
108
99
  createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
109
100
  fromContract: existing?.fromContract ?? null,
110
101
  toContract: existing?.toContract ?? { storage: { storageHash: meta.to } },
111
- hints: existing?.hints ?? {
112
- used: [],
113
- applied: [],
114
- plannerVersion: "2.0.0",
115
- planningStrategy: "class-based"
116
- },
102
+ hints: normalizeHints(existing?.hints),
117
103
  ...ifDefined("authorship", existing?.authorship)
118
104
  };
119
105
  const migrationId = computeMigrationId(baseManifest, ops);
@@ -122,38 +108,42 @@ function buildAttestedManifest(migrationDir, meta, ops) {
122
108
  migrationId
123
109
  };
124
110
  }
125
- function readExistingManifest(manifestPath) {
126
- let raw;
127
- try {
128
- raw = readFileSync(manifestPath, "utf-8");
129
- } catch {
130
- return null;
131
- }
132
- try {
133
- return JSON.parse(raw);
134
- } catch {
135
- return null;
136
- }
111
+ /**
112
+ * Project `existing.hints` down to the known `MigrationHints` shape, dropping
113
+ * any legacy keys that may linger in manifests scaffolded by older CLI
114
+ * versions (e.g. `planningStrategy`). Picking fields explicitly instead of
115
+ * spreading keeps refreshed `migration.json` files schema-clean regardless
116
+ * of what was on disk before.
117
+ */
118
+ function normalizeHints(existing) {
119
+ return {
120
+ used: existing?.used ?? [],
121
+ applied: existing?.applied ?? [],
122
+ plannerVersion: existing?.plannerVersion ?? "2.0.0"
123
+ };
137
124
  }
138
- function serializeMigration(MigrationClass, migrationDir, dryRun) {
139
- const instance = new MigrationClass();
125
+ /**
126
+ * Pure conversion from a `Migration` instance (plus the previously
127
+ * scaffolded manifest, when one exists on disk) to the in-memory
128
+ * artifacts that downstream tooling persists. Owns metadata validation,
129
+ * manifest synthesis/preservation, hint normalization, and the
130
+ * content-addressed `migrationId` computation, but performs no file I/O
131
+ * — callers handle reads (to source `existing`) and writes (to persist
132
+ * `opsJson` / `manifestJson`).
133
+ */
134
+ function buildMigrationArtifacts(instance, existing) {
140
135
  const ops = instance.operations;
141
136
  if (!Array.isArray(ops)) throw new Error("operations must be an array");
142
- const serializedOps = JSON.stringify(ops, null, 2);
143
137
  const parsed = MigrationMetaSchema(instance.describe());
144
138
  if (parsed instanceof type.errors) throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
145
- const manifest = buildAttestedManifest(migrationDir, parsed, ops);
146
- if (dryRun) {
147
- process.stdout.write(`--- migration.json ---\n${JSON.stringify(manifest, null, 2)}\n`);
148
- process.stdout.write("--- ops.json ---\n");
149
- process.stdout.write(`${serializedOps}\n`);
150
- return;
151
- }
152
- writeFileSync(join(migrationDir, "ops.json"), serializedOps);
153
- writeFileSync(join(migrationDir, "migration.json"), JSON.stringify(manifest, null, 2));
154
- process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\n`);
139
+ const manifest = buildAttestedManifest(parsed, ops, existing);
140
+ return {
141
+ opsJson: JSON.stringify(ops, null, 2),
142
+ manifest,
143
+ manifestJson: JSON.stringify(manifest, null, 2)
144
+ };
155
145
  }
156
146
 
157
147
  //#endregion
158
- export { Migration };
148
+ export { Migration, buildMigrationArtifacts, isDirectEntrypoint, printMigrationHelp };
159
149
  //# sourceMappingURL=migration.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"migration.mjs","names":["isEntrypoint: boolean","baseManifest: MigrationManifest","raw: string"],"sources":["../../src/migration-base.ts"],"sourcesContent":["import { readFileSync, realpathSync, writeFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type { Contract } from '@prisma-next/contract/types';\nimport type {\n MigrationPlan,\n MigrationPlanOperation,\n} from '@prisma-next/framework-components/control';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { type } from 'arktype';\nimport { dirname, join } from 'pathe';\nimport { computeMigrationId } from './attestation';\nimport type { MigrationManifest, MigrationOps } from './types';\n\nexport interface MigrationMeta {\n readonly from: string;\n readonly to: string;\n readonly kind?: 'regular' | 'baseline';\n readonly labels?: readonly string[];\n}\n\nconst MigrationMetaSchema = type({\n from: 'string',\n to: 'string',\n 'kind?': \"'regular' | 'baseline'\",\n 'labels?': type('string').array(),\n});\n\n/**\n * Base class for class-flow migrations.\n *\n * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the\n * runner can consume it directly via `targetId`, `operations`, `origin`, and\n * `destination`. The manifest-shaped inputs come from `describe()`, which\n * every migration must implement — `migration.json` is required for a\n * migration to be valid.\n */\nexport abstract class Migration<TOperation extends MigrationPlanOperation = MigrationPlanOperation>\n implements MigrationPlan\n{\n abstract readonly targetId: string;\n\n /**\n * Ordered list of operations this migration performs.\n *\n * Implemented as a getter so that subclasses can either precompute the list\n * in their constructor or build it lazily per access.\n */\n abstract get operations(): readonly TOperation[];\n\n /**\n * Metadata inputs used to build `migration.json` and to derive the plan's\n * origin/destination identities. Every migration must provide this —\n * omitting it would produce an invalid on-disk migration package.\n */\n abstract describe(): MigrationMeta;\n\n get origin(): { readonly storageHash: string } | null {\n const from = this.describe().from;\n // An empty `from` represents a migration with no prior origin (e.g.\n // initial baseline, or an in-process plan that was never persisted).\n // Surface that as a null origin so runners treat the plan as\n // origin-less rather than matching against an empty storage hash.\n return from === '' ? null : { storageHash: from };\n }\n\n get destination(): { readonly storageHash: string } {\n return { storageHash: this.describe().to };\n }\n\n /**\n * Entrypoint guard for migration files. When called at module scope,\n * detects whether the file is being run directly (e.g. `node migration.ts`)\n * and if so, serializes the migration plan to `ops.json` and\n * `migration.json` in the same directory. When the file is imported by\n * another module, this is a no-op.\n *\n * Usage (at module scope, after the class definition):\n *\n * class MyMigration extends Migration { ... }\n * export default MyMigration;\n * Migration.run(import.meta.url, MyMigration);\n */\n static run(importMetaUrl: string, MigrationClass: new () => Migration): void {\n if (!importMetaUrl) return;\n\n const metaFilename = fileURLToPath(importMetaUrl);\n const argv1 = process.argv[1];\n if (!argv1) return;\n\n let isEntrypoint: boolean;\n try {\n isEntrypoint = realpathSync(metaFilename) === realpathSync(argv1);\n } catch {\n return;\n }\n if (!isEntrypoint) return;\n\n const args = process.argv.slice(2);\n\n if (args.includes('--help')) {\n printHelp();\n return;\n }\n\n const dryRun = args.includes('--dry-run');\n const migrationDir = dirname(metaFilename);\n\n try {\n serializeMigration(MigrationClass, migrationDir, dryRun);\n } catch (err) {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`);\n process.exitCode = 1;\n }\n }\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n 'Usage: node <migration-file> [options]',\n '',\n 'Options:',\n ' --dry-run Print operations to stdout without writing files',\n ' --help Show this help message',\n '',\n ].join('\\n'),\n );\n}\n\n/**\n * Build the attested manifest written by `Migration.run()`.\n *\n * When a `migration.json` already exists in the directory (the common case:\n * the package was scaffolded by `migration plan`), preserve the contract\n * bookends, hints, labels, and `createdAt` set there — those fields are\n * owned by the CLI scaffolder, not the authored class. Only the\n * `describe()`-derived fields (`from`, `to`, `kind`) and the operations\n * change as the author iterates. When no manifest exists yet (a bare\n * `migration.ts` run from scratch), synthesize a minimal but\n * schema-conformant manifest so the resulting package can still be read,\n * verified, and applied.\n *\n * In both cases the `migrationId` is recomputed against the current\n * manifest + ops so the on-disk artifacts are always fully attested — no\n * draft (`migrationId: null`) ever leaves this function.\n */\nfunction buildAttestedManifest(\n migrationDir: string,\n meta: MigrationMeta,\n ops: MigrationOps,\n): MigrationManifest {\n const existing = readExistingManifest(join(migrationDir, 'migration.json'));\n\n const baseManifest: MigrationManifest = {\n migrationId: null,\n from: meta.from,\n to: meta.to,\n kind: meta.kind ?? 'regular',\n labels: meta.labels ?? existing?.labels ?? [],\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n fromContract: existing?.fromContract ?? null,\n // When no scaffolded manifest exists we synthesize a minimal contract\n // stub so the package is still readable end-to-end. The cast is\n // intentional: only the storage bookend matters for hash computation\n // (everything else is stripped by `computeMigrationId`), and a real\n // contract bookend would only be available after `migration plan`.\n toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),\n hints: existing?.hints ?? {\n used: [],\n applied: [],\n plannerVersion: '2.0.0',\n planningStrategy: 'class-based',\n },\n ...ifDefined('authorship', existing?.authorship),\n };\n\n const migrationId = computeMigrationId(baseManifest, ops);\n return { ...baseManifest, migrationId };\n}\n\nfunction readExistingManifest(manifestPath: string): Partial<MigrationManifest> | null {\n let raw: string;\n try {\n raw = readFileSync(manifestPath, 'utf-8');\n } catch {\n return null;\n }\n try {\n return JSON.parse(raw) as Partial<MigrationManifest>;\n } catch {\n return null;\n }\n}\n\nfunction serializeMigration(\n MigrationClass: new () => Migration,\n migrationDir: string,\n dryRun: boolean,\n): void {\n const instance = new MigrationClass();\n\n const ops = instance.operations;\n\n if (!Array.isArray(ops)) {\n throw new Error('operations must be an array');\n }\n\n const serializedOps = JSON.stringify(ops, null, 2);\n\n const rawMeta: unknown = instance.describe();\n const parsed = MigrationMetaSchema(rawMeta);\n if (parsed instanceof type.errors) {\n throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);\n }\n\n const manifest = buildAttestedManifest(migrationDir, parsed, ops);\n\n if (dryRun) {\n process.stdout.write(`--- migration.json ---\\n${JSON.stringify(manifest, null, 2)}\\n`);\n process.stdout.write('--- ops.json ---\\n');\n process.stdout.write(`${serializedOps}\\n`);\n return;\n }\n\n writeFileSync(join(migrationDir, 'ops.json'), serializedOps);\n writeFileSync(join(migrationDir, 'migration.json'), JSON.stringify(manifest, null, 2));\n\n process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\\n`);\n}\n"],"mappings":";;;;;;;;;AAoBA,MAAM,sBAAsB,KAAK;CAC/B,MAAM;CACN,IAAI;CACJ,SAAS;CACT,WAAW,KAAK,SAAS,CAAC,OAAO;CAClC,CAAC;;;;;;;;;;AAWF,IAAsB,YAAtB,MAEA;CAkBE,IAAI,SAAkD;EACpD,MAAM,OAAO,KAAK,UAAU,CAAC;AAK7B,SAAO,SAAS,KAAK,OAAO,EAAE,aAAa,MAAM;;CAGnD,IAAI,cAAgD;AAClD,SAAO,EAAE,aAAa,KAAK,UAAU,CAAC,IAAI;;;;;;;;;;;;;;;CAgB5C,OAAO,IAAI,eAAuB,gBAA2C;AAC3E,MAAI,CAAC,cAAe;EAEpB,MAAM,eAAe,cAAc,cAAc;EACjD,MAAM,QAAQ,QAAQ,KAAK;AAC3B,MAAI,CAAC,MAAO;EAEZ,IAAIA;AACJ,MAAI;AACF,kBAAe,aAAa,aAAa,KAAK,aAAa,MAAM;UAC3D;AACN;;AAEF,MAAI,CAAC,aAAc;EAEnB,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAElC,MAAI,KAAK,SAAS,SAAS,EAAE;AAC3B,cAAW;AACX;;EAGF,MAAM,SAAS,KAAK,SAAS,YAAY;EACzC,MAAM,eAAe,QAAQ,aAAa;AAE1C,MAAI;AACF,sBAAmB,gBAAgB,cAAc,OAAO;WACjD,KAAK;AACZ,WAAQ,OAAO,MAAM,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,IAAI;AAC7E,WAAQ,WAAW;;;;AAKzB,SAAS,YAAkB;AACzB,SAAQ,OAAO,MACb;EACE;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,CACb;;;;;;;;;;;;;;;;;;;AAoBH,SAAS,sBACP,cACA,MACA,KACmB;CACnB,MAAM,WAAW,qBAAqB,KAAK,cAAc,iBAAiB,CAAC;CAE3E,MAAMC,eAAkC;EACtC,aAAa;EACb,MAAM,KAAK;EACX,IAAI,KAAK;EACT,MAAM,KAAK,QAAQ;EACnB,QAAQ,KAAK,UAAU,UAAU,UAAU,EAAE;EAC7C,WAAW,UAAU,8BAAa,IAAI,MAAM,EAAC,aAAa;EAC1D,cAAc,UAAU,gBAAgB;EAMxC,YAAY,UAAU,cAAe,EAAE,SAAS,EAAE,aAAa,KAAK,IAAI,EAAE;EAC1E,OAAO,UAAU,SAAS;GACxB,MAAM,EAAE;GACR,SAAS,EAAE;GACX,gBAAgB;GAChB,kBAAkB;GACnB;EACD,GAAG,UAAU,cAAc,UAAU,WAAW;EACjD;CAED,MAAM,cAAc,mBAAmB,cAAc,IAAI;AACzD,QAAO;EAAE,GAAG;EAAc;EAAa;;AAGzC,SAAS,qBAAqB,cAAyD;CACrF,IAAIC;AACJ,KAAI;AACF,QAAM,aAAa,cAAc,QAAQ;SACnC;AACN,SAAO;;AAET,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO;;;AAIX,SAAS,mBACP,gBACA,cACA,QACM;CACN,MAAM,WAAW,IAAI,gBAAgB;CAErC,MAAM,MAAM,SAAS;AAErB,KAAI,CAAC,MAAM,QAAQ,IAAI,CACrB,OAAM,IAAI,MAAM,8BAA8B;CAGhD,MAAM,gBAAgB,KAAK,UAAU,KAAK,MAAM,EAAE;CAGlD,MAAM,SAAS,oBADU,SAAS,UAAU,CACD;AAC3C,KAAI,kBAAkB,KAAK,OACzB,OAAM,IAAI,MAAM,yCAAyC,OAAO,UAAU;CAG5E,MAAM,WAAW,sBAAsB,cAAc,QAAQ,IAAI;AAEjE,KAAI,QAAQ;AACV,UAAQ,OAAO,MAAM,2BAA2B,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC,IAAI;AACtF,UAAQ,OAAO,MAAM,qBAAqB;AAC1C,UAAQ,OAAO,MAAM,GAAG,cAAc,IAAI;AAC1C;;AAGF,eAAc,KAAK,cAAc,WAAW,EAAE,cAAc;AAC5D,eAAc,KAAK,cAAc,iBAAiB,EAAE,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC;AAEtF,SAAQ,OAAO,MAAM,sCAAsC,aAAa,IAAI"}
1
+ {"version":3,"file":"migration.mjs","names":["baseManifest: Omit<MigrationManifest, 'migrationId'>"],"sources":["../../src/migration-base.ts"],"sourcesContent":["import { realpathSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type { Contract } from '@prisma-next/contract/types';\nimport type {\n ControlStack,\n MigrationPlan,\n MigrationPlanOperation,\n} from '@prisma-next/framework-components/control';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { type } from 'arktype';\nimport { computeMigrationId } from './attestation';\nimport type { MigrationHints, MigrationManifest, MigrationOps } from './types';\n\nexport interface MigrationMeta {\n readonly from: string;\n readonly to: string;\n readonly kind?: 'regular' | 'baseline';\n readonly labels?: readonly string[];\n}\n\nconst MigrationMetaSchema = type({\n from: 'string',\n to: 'string',\n 'kind?': \"'regular' | 'baseline'\",\n 'labels?': type('string').array(),\n});\n\n/**\n * Base class for migrations.\n *\n * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the\n * runner can consume it directly via `targetId`, `operations`, `origin`, and\n * `destination`. The manifest-shaped inputs come from `describe()`, which\n * every migration must implement — `migration.json` is required for a\n * migration to be valid.\n */\nexport abstract class Migration<\n TOperation extends MigrationPlanOperation = MigrationPlanOperation,\n TFamilyId extends string = string,\n TTargetId extends string = string,\n> implements MigrationPlan\n{\n abstract readonly targetId: string;\n\n /**\n * Assembled `ControlStack` injected by the orchestrator (`runMigration`).\n *\n * Subclasses (e.g. `PostgresMigration`) read the stack to materialize their\n * adapter once per instance. Optional at the abstract level so unit tests can\n * construct `Migration` instances purely for `operations` / `describe`\n * assertions without needing a real stack; concrete subclasses that need the\n * stack at runtime should narrow the parameter to required.\n */\n protected readonly stack: ControlStack<TFamilyId, TTargetId> | undefined;\n\n constructor(stack?: ControlStack<TFamilyId, TTargetId>) {\n this.stack = stack;\n }\n\n /**\n * Ordered list of operations this migration performs.\n *\n * Implemented as a getter so that subclasses can either precompute the list\n * in their constructor or build it lazily per access.\n */\n abstract get operations(): readonly TOperation[];\n\n /**\n * Metadata inputs used to build `migration.json` and to derive the plan's\n * origin/destination identities. Every migration must provide this —\n * omitting it would produce an invalid on-disk migration package.\n */\n abstract describe(): MigrationMeta;\n\n get origin(): { readonly storageHash: string } | null {\n const from = this.describe().from;\n // An empty `from` represents a migration with no prior origin (e.g.\n // initial baseline, or an in-process plan that was never persisted).\n // Surface that as a null origin so runners treat the plan as\n // origin-less rather than matching against an empty storage hash.\n return from === '' ? null : { storageHash: from };\n }\n\n get destination(): { readonly storageHash: string } {\n return { storageHash: this.describe().to };\n }\n}\n\n/**\n * Returns true when `import.meta.url` resolves to the same file that was\n * invoked as the node entrypoint (`process.argv[1]`). Used by\n * `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when\n * the migration module is being imported (e.g. by another script) rather\n * than executed directly.\n */\nexport function isDirectEntrypoint(importMetaUrl: string): boolean {\n const metaFilename = fileURLToPath(importMetaUrl);\n const argv1 = process.argv[1];\n if (!argv1) return false;\n try {\n return realpathSync(metaFilename) === realpathSync(argv1);\n } catch {\n return false;\n }\n}\n\nexport function printMigrationHelp(): void {\n printHelp();\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n 'Usage: node <migration-file> [options]',\n '',\n 'Options:',\n ' --dry-run Print operations to stdout without writing files',\n ' --help Show this help message',\n '',\n ].join('\\n'),\n );\n}\n\n/**\n * In-memory artifacts produced from a `Migration` instance: the\n * serialized `ops.json` body, the `migration.json` manifest object, and\n * its serialized form. Returned by `buildMigrationArtifacts` so callers\n * (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can\n * decide how to persist them — write to disk, print in dry-run, ship\n * over the wire — without coupling artifact construction to file I/O.\n */\nexport interface MigrationArtifacts {\n readonly opsJson: string;\n readonly manifest: MigrationManifest;\n readonly manifestJson: string;\n}\n\n/**\n * Build the attested manifest from `describe()`-derived metadata, the\n * operations list, and the previously-scaffolded manifest (if any).\n *\n * When a `migration.json` already exists for this package (the common\n * case: it was scaffolded by `migration plan`), preserve the contract\n * bookends, hints, labels, and `createdAt` set there — those fields are\n * owned by the CLI scaffolder, not the authored class. Only the\n * `describe()`-derived fields (`from`, `to`, `kind`) and the operations\n * change as the author iterates. When no manifest exists yet (a bare\n * `migration.ts` run from scratch), synthesize a minimal but\n * schema-conformant manifest so the resulting package can still be read,\n * verified, and applied.\n *\n * The `migrationId` is recomputed against the current manifest + ops so\n * the on-disk artifacts are always fully attested.\n */\nfunction buildAttestedManifest(\n meta: MigrationMeta,\n ops: MigrationOps,\n existing: Partial<MigrationManifest> | null,\n): MigrationManifest {\n const baseManifest: Omit<MigrationManifest, 'migrationId'> = {\n from: meta.from,\n to: meta.to,\n kind: meta.kind ?? 'regular',\n labels: meta.labels ?? existing?.labels ?? [],\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n fromContract: existing?.fromContract ?? null,\n // When no scaffolded manifest exists we synthesize a minimal contract\n // stub so the package is still readable end-to-end. The cast is\n // intentional: only the storage bookend matters for hash computation\n // (everything else is stripped by `computeMigrationId`), and a real\n // contract bookend would only be available after `migration plan`.\n toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),\n hints: normalizeHints(existing?.hints),\n ...ifDefined('authorship', existing?.authorship),\n };\n\n const migrationId = computeMigrationId(baseManifest, ops);\n return { ...baseManifest, migrationId };\n}\n\n/**\n * Project `existing.hints` down to the known `MigrationHints` shape, dropping\n * any legacy keys that may linger in manifests scaffolded by older CLI\n * versions (e.g. `planningStrategy`). Picking fields explicitly instead of\n * spreading keeps refreshed `migration.json` files schema-clean regardless\n * of what was on disk before.\n */\nfunction normalizeHints(existing: MigrationHints | undefined): MigrationHints {\n return {\n used: existing?.used ?? [],\n applied: existing?.applied ?? [],\n plannerVersion: existing?.plannerVersion ?? '2.0.0',\n };\n}\n\n/**\n * Pure conversion from a `Migration` instance (plus the previously\n * scaffolded manifest, when one exists on disk) to the in-memory\n * artifacts that downstream tooling persists. Owns metadata validation,\n * manifest synthesis/preservation, hint normalization, and the\n * content-addressed `migrationId` computation, but performs no file I/O\n * — callers handle reads (to source `existing`) and writes (to persist\n * `opsJson` / `manifestJson`).\n */\nexport function buildMigrationArtifacts(\n instance: Migration,\n existing: Partial<MigrationManifest> | null,\n): MigrationArtifacts {\n const ops = instance.operations;\n if (!Array.isArray(ops)) {\n throw new Error('operations must be an array');\n }\n\n const rawMeta: unknown = instance.describe();\n const parsed = MigrationMetaSchema(rawMeta);\n if (parsed instanceof type.errors) {\n throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);\n }\n\n const manifest = buildAttestedManifest(parsed, ops, existing);\n\n return {\n opsJson: JSON.stringify(ops, null, 2),\n manifest,\n manifestJson: JSON.stringify(manifest, null, 2),\n };\n}\n"],"mappings":";;;;;;;;AAoBA,MAAM,sBAAsB,KAAK;CAC/B,MAAM;CACN,IAAI;CACJ,SAAS;CACT,WAAW,KAAK,SAAS,CAAC,OAAO;CAClC,CAAC;;;;;;;;;;AAWF,IAAsB,YAAtB,MAKA;;;;;;;;;;CAYE,AAAmB;CAEnB,YAAY,OAA4C;AACtD,OAAK,QAAQ;;CAkBf,IAAI,SAAkD;EACpD,MAAM,OAAO,KAAK,UAAU,CAAC;AAK7B,SAAO,SAAS,KAAK,OAAO,EAAE,aAAa,MAAM;;CAGnD,IAAI,cAAgD;AAClD,SAAO,EAAE,aAAa,KAAK,UAAU,CAAC,IAAI;;;;;;;;;;AAW9C,SAAgB,mBAAmB,eAAgC;CACjE,MAAM,eAAe,cAAc,cAAc;CACjD,MAAM,QAAQ,QAAQ,KAAK;AAC3B,KAAI,CAAC,MAAO,QAAO;AACnB,KAAI;AACF,SAAO,aAAa,aAAa,KAAK,aAAa,MAAM;SACnD;AACN,SAAO;;;AAIX,SAAgB,qBAA2B;AACzC,YAAW;;AAGb,SAAS,YAAkB;AACzB,SAAQ,OAAO,MACb;EACE;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,CACb;;;;;;;;;;;;;;;;;;;AAkCH,SAAS,sBACP,MACA,KACA,UACmB;CACnB,MAAMA,eAAuD;EAC3D,MAAM,KAAK;EACX,IAAI,KAAK;EACT,MAAM,KAAK,QAAQ;EACnB,QAAQ,KAAK,UAAU,UAAU,UAAU,EAAE;EAC7C,WAAW,UAAU,8BAAa,IAAI,MAAM,EAAC,aAAa;EAC1D,cAAc,UAAU,gBAAgB;EAMxC,YAAY,UAAU,cAAe,EAAE,SAAS,EAAE,aAAa,KAAK,IAAI,EAAE;EAC1E,OAAO,eAAe,UAAU,MAAM;EACtC,GAAG,UAAU,cAAc,UAAU,WAAW;EACjD;CAED,MAAM,cAAc,mBAAmB,cAAc,IAAI;AACzD,QAAO;EAAE,GAAG;EAAc;EAAa;;;;;;;;;AAUzC,SAAS,eAAe,UAAsD;AAC5E,QAAO;EACL,MAAM,UAAU,QAAQ,EAAE;EAC1B,SAAS,UAAU,WAAW,EAAE;EAChC,gBAAgB,UAAU,kBAAkB;EAC7C;;;;;;;;;;;AAYH,SAAgB,wBACd,UACA,UACoB;CACpB,MAAM,MAAM,SAAS;AACrB,KAAI,CAAC,MAAM,QAAQ,IAAI,CACrB,OAAM,IAAI,MAAM,8BAA8B;CAIhD,MAAM,SAAS,oBADU,SAAS,UAAU,CACD;AAC3C,KAAI,kBAAkB,KAAK,OACzB,OAAM,IAAI,MAAM,yCAAyC,OAAO,UAAU;CAG5E,MAAM,WAAW,sBAAsB,QAAQ,KAAK,SAAS;AAE7D,QAAO;EACL,SAAS,KAAK,UAAU,KAAK,MAAM,EAAE;EACrC;EACA,cAAc,KAAK,UAAU,UAAU,MAAM,EAAE;EAChD"}
@@ -1,10 +1,16 @@
1
1
  //#region src/refs.d.ts
2
- type Refs = Readonly<Record<string, string>>;
2
+ interface RefEntry {
3
+ readonly hash: string;
4
+ readonly invariants: readonly string[];
5
+ }
6
+ type Refs = Readonly<Record<string, RefEntry>>;
3
7
  declare function validateRefName(name: string): boolean;
4
8
  declare function validateRefValue(value: string): boolean;
5
- declare function readRefs(refsPath: string): Promise<Refs>;
6
- declare function writeRefs(refsPath: string, refs: Refs): Promise<void>;
7
- declare function resolveRef(refs: Refs, name: string): string;
9
+ declare function readRef(refsDir: string, name: string): Promise<RefEntry>;
10
+ declare function readRefs(refsDir: string): Promise<Refs>;
11
+ declare function writeRef(refsDir: string, name: string, entry: RefEntry): Promise<void>;
12
+ declare function deleteRef(refsDir: string, name: string): Promise<void>;
13
+ declare function resolveRef(refs: Refs, name: string): RefEntry;
8
14
  //#endregion
9
- export { type Refs, readRefs, resolveRef, validateRefName, validateRefValue, writeRefs };
15
+ export { type RefEntry, type Refs, deleteRef, readRef, readRefs, resolveRef, validateRefName, validateRefValue, writeRef };
10
16
  //# sourceMappingURL=refs.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"refs.d.mts","names":[],"sources":["../../src/refs.ts"],"sourcesContent":[],"mappings":";KAUY,IAAA,GAAO,SAAS;AAAhB,iBAKI,eAAA,CALW,IAAA,EAAA,MAAA,CAAA,EAAA,OAAA;AAKX,iBAQA,gBAAA,CARe,KAAA,EAAA,MAAA,CAAA,EAAA,OAAA;AAQf,iBAaM,QAAA,CAbU,QAAA,EAAA,MAAA,CAAA,EAakB,OAblB,CAa0B,IAb1B,CAAA;AAaV,iBA0BA,SAAA,CA1B4B,QAAO,EAAA,MAAA,EAAA,IAAA,EA0BD,IA1BC,CAAA,EA0BM,OA1BN,CAAA,IAAA,CAAA;AA0BnC,iBAoBN,UAAA,CApB+C,IAAA,EAoB9B,IApBqC,EAAA,IAAA,EAAA,MAAA,CAAA,EAAA,MAAA"}
1
+ {"version":3,"file":"refs.d.mts","names":[],"sources":["../../src/refs.ts"],"sourcesContent":[],"mappings":";UAUiB,QAAA;EAAA,SAAA,IAAQ,EAAA,MAAA;EAKb,SAAI,UAAA,EAAA,SAAA,MAAA,EAAA;;AAAY,KAAhB,IAAA,GAAO,QAAS,CAAA,MAAA,CAAA,MAAA,EAAe,QAAf,CAAA,CAAA;AAAT,iBAKH,eAAA,CALG,IAAA,EAAA,MAAA,CAAA,EAAA,OAAA;AAAQ,iBAaX,gBAAA,CAbW,KAAA,EAAA,MAAA,CAAA,EAAA,OAAA;AAKX,iBA8BM,OAAA,CA9BS,OAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,CAAA,EA8B+B,OA9B/B,CA8BuC,QA9BvC,CAAA;AAQf,iBAyDM,QAAA,CAzDU,OAAA,EAAA,MAAA,CAAA,EAyDiB,OAzDjB,CAyDyB,IAzDzB,CAAA;AAsBV,iBAsFA,QAAA,CAtFgD,OAAR,EAAA,MAAO,EAAA,IAAA,EAAA,MAAA,EAAA,KAAA,EAsFA,QAtFA,CAAA,EAsFW,OAtFX,CAAA,IAAA,CAAA;AAmC/C,iBAuEA,SAAA,CAvE2B,OAAO,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,CAAA,EAuEQ,OAvER,CAAA,IAAA,CAAA;AAmDlC,iBA0DN,UAAA,CA1DqD,IAAW,EA0D/C,IA1DsD,EAAA,IAAA,EAAA,MAAA,CAAA,EA0DjC,QA1DiC"}