@prisma-next/migration-tools 0.4.1 → 0.4.3

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 (100) hide show
  1. package/README.md +34 -22
  2. package/dist/{constants-BRi0X7B_.mjs → constants-BQEHsaEx.mjs} +1 -1
  3. package/dist/{constants-BRi0X7B_.mjs.map → constants-BQEHsaEx.mjs.map} +1 -1
  4. package/dist/errors-CfmjBeK0.mjs +272 -0
  5. package/dist/errors-CfmjBeK0.mjs.map +1 -0
  6. package/dist/exports/constants.mjs +1 -1
  7. package/dist/exports/errors.d.mts +63 -0
  8. package/dist/exports/errors.d.mts.map +1 -0
  9. package/dist/exports/errors.mjs +3 -0
  10. package/dist/exports/graph.d.mts +2 -0
  11. package/dist/exports/graph.mjs +1 -0
  12. package/dist/exports/hash.d.mts +52 -0
  13. package/dist/exports/hash.d.mts.map +1 -0
  14. package/dist/exports/hash.mjs +3 -0
  15. package/dist/exports/invariants.d.mts +24 -0
  16. package/dist/exports/invariants.d.mts.map +1 -0
  17. package/dist/exports/invariants.mjs +4 -0
  18. package/dist/exports/io.d.mts +7 -6
  19. package/dist/exports/io.d.mts.map +1 -1
  20. package/dist/exports/io.mjs +162 -2
  21. package/dist/exports/io.mjs.map +1 -0
  22. package/dist/exports/metadata.d.mts +2 -0
  23. package/dist/exports/metadata.mjs +1 -0
  24. package/dist/exports/migration-graph.d.mts +124 -0
  25. package/dist/exports/migration-graph.d.mts.map +1 -0
  26. package/dist/exports/migration-graph.mjs +526 -0
  27. package/dist/exports/migration-graph.mjs.map +1 -0
  28. package/dist/exports/migration-ts.d.mts +5 -1
  29. package/dist/exports/migration-ts.d.mts.map +1 -1
  30. package/dist/exports/migration-ts.mjs +6 -2
  31. package/dist/exports/migration-ts.mjs.map +1 -1
  32. package/dist/exports/migration.d.mts +51 -20
  33. package/dist/exports/migration.d.mts.map +1 -1
  34. package/dist/exports/migration.mjs +110 -99
  35. package/dist/exports/migration.mjs.map +1 -1
  36. package/dist/exports/package.d.mts +2 -0
  37. package/dist/exports/package.mjs +1 -0
  38. package/dist/exports/refs.d.mts +11 -5
  39. package/dist/exports/refs.d.mts.map +1 -1
  40. package/dist/exports/refs.mjs +106 -30
  41. package/dist/exports/refs.mjs.map +1 -1
  42. package/dist/graph-BHPv-9Gl.d.mts +28 -0
  43. package/dist/graph-BHPv-9Gl.d.mts.map +1 -0
  44. package/dist/hash-BARZdVgW.mjs +76 -0
  45. package/dist/hash-BARZdVgW.mjs.map +1 -0
  46. package/dist/invariants-30VA65sB.mjs +42 -0
  47. package/dist/invariants-30VA65sB.mjs.map +1 -0
  48. package/dist/metadata-BP1cmU7Z.d.mts +50 -0
  49. package/dist/metadata-BP1cmU7Z.d.mts.map +1 -0
  50. package/dist/op-schema-DZKFua46.mjs +14 -0
  51. package/dist/op-schema-DZKFua46.mjs.map +1 -0
  52. package/dist/package-5HCCg0z-.d.mts +21 -0
  53. package/dist/package-5HCCg0z-.d.mts.map +1 -0
  54. package/package.json +30 -14
  55. package/src/errors.ts +210 -17
  56. package/src/exports/errors.ts +7 -0
  57. package/src/exports/graph.ts +1 -0
  58. package/src/exports/hash.ts +2 -0
  59. package/src/exports/invariants.ts +1 -0
  60. package/src/exports/io.ts +1 -1
  61. package/src/exports/metadata.ts +1 -0
  62. package/src/exports/{dag.ts → migration-graph.ts} +3 -2
  63. package/src/exports/migration.ts +7 -1
  64. package/src/exports/package.ts +1 -0
  65. package/src/exports/refs.ts +10 -2
  66. package/src/graph-ops.ts +57 -30
  67. package/src/graph.ts +25 -0
  68. package/src/hash.ts +91 -0
  69. package/src/invariants.ts +45 -0
  70. package/src/io.ts +57 -31
  71. package/src/metadata.ts +41 -0
  72. package/src/migration-base.ts +155 -124
  73. package/src/migration-graph.ts +676 -0
  74. package/src/migration-ts.ts +5 -1
  75. package/src/op-schema.ts +11 -0
  76. package/src/package.ts +18 -0
  77. package/src/refs.ts +148 -37
  78. package/dist/attestation-DtF8tEOM.mjs +0 -65
  79. package/dist/attestation-DtF8tEOM.mjs.map +0 -1
  80. package/dist/errors-BKbRGCJM.mjs +0 -160
  81. package/dist/errors-BKbRGCJM.mjs.map +0 -1
  82. package/dist/exports/attestation.d.mts +0 -37
  83. package/dist/exports/attestation.d.mts.map +0 -1
  84. package/dist/exports/attestation.mjs +0 -4
  85. package/dist/exports/dag.d.mts +0 -51
  86. package/dist/exports/dag.d.mts.map +0 -1
  87. package/dist/exports/dag.mjs +0 -386
  88. package/dist/exports/dag.mjs.map +0 -1
  89. package/dist/exports/types.d.mts +0 -35
  90. package/dist/exports/types.d.mts.map +0 -1
  91. package/dist/exports/types.mjs +0 -3
  92. package/dist/io-CCnYsUHU.mjs +0 -153
  93. package/dist/io-CCnYsUHU.mjs.map +0 -1
  94. package/dist/types-DyGXcWWp.d.mts +0 -71
  95. package/dist/types-DyGXcWWp.d.mts.map +0 -1
  96. package/src/attestation.ts +0 -81
  97. package/src/dag.ts +0 -426
  98. package/src/exports/attestation.ts +0 -2
  99. package/src/exports/types.ts +0 -10
  100. package/src/types.ts +0 -66
@@ -1,10 +1,10 @@
1
- import { MigrationPlan, MigrationPlanOperation } from "@prisma-next/framework-components/control";
1
+ import { n as MigrationMetadata } from "../metadata-BP1cmU7Z.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 {
5
- readonly from: string;
6
+ readonly from: string | null;
6
7
  readonly to: string;
7
- readonly kind?: 'regular' | 'baseline';
8
8
  readonly labels?: readonly string[];
9
9
  }
10
10
  /**
@@ -12,12 +12,23 @@ interface MigrationMeta {
12
12
  *
13
13
  * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
14
14
  * runner can consume it directly via `targetId`, `operations`, `origin`, and
15
- * `destination`. The manifest-shaped inputs come from `describe()`, which
15
+ * `destination`. The metadata-shaped inputs come from `describe()`, which
16
16
  * every migration must implement — `migration.json` is required for a
17
17
  * migration to be valid.
18
18
  */
19
- declare abstract class Migration<TOperation extends MigrationPlanOperation = MigrationPlanOperation> implements MigrationPlan {
19
+ declare abstract class Migration<TOperation extends MigrationPlanOperation = MigrationPlanOperation, TFamilyId extends string = string, TTargetId extends string = string> implements MigrationPlan {
20
20
  abstract readonly targetId: string;
21
+ /**
22
+ * Assembled `ControlStack` injected by the orchestrator (`runMigration`).
23
+ *
24
+ * Subclasses (e.g. `PostgresMigration`) read the stack to materialize their
25
+ * adapter once per instance. Optional at the abstract level so unit tests can
26
+ * construct `Migration` instances purely for `operations` / `describe`
27
+ * assertions without needing a real stack; concrete subclasses that need the
28
+ * stack at runtime should narrow the parameter to required.
29
+ */
30
+ protected readonly stack: ControlStack<TFamilyId, TTargetId> | undefined;
31
+ constructor(stack?: ControlStack<TFamilyId, TTargetId>);
21
32
  /**
22
33
  * Ordered list of operations this migration performs.
23
34
  *
@@ -37,21 +48,41 @@ declare abstract class Migration<TOperation extends MigrationPlanOperation = Mig
37
48
  get destination(): {
38
49
  readonly storageHash: string;
39
50
  };
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
51
  }
52
+ /**
53
+ * Returns true when `import.meta.url` resolves to the same file that was
54
+ * invoked as the node entrypoint (`process.argv[1]`). Used by
55
+ * `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when
56
+ * the migration module is being imported (e.g. by another script) rather
57
+ * than executed directly.
58
+ */
59
+ declare function isDirectEntrypoint(importMetaUrl: string): boolean;
60
+ /**
61
+ * In-memory artifacts produced from a `Migration` instance: the
62
+ * serialized `ops.json` body, the `migration.json` metadata object, and
63
+ * its serialized form. Returned by `buildMigrationArtifacts` so callers
64
+ * (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can
65
+ * decide how to persist them — write to disk, print in dry-run, ship
66
+ * over the wire — without coupling artifact construction to file I/O.
67
+ *
68
+ * `metadataJson` is `JSON.stringify(metadata, null, 2)` — the canonical
69
+ * on-disk shape that the arktype loader-schema in `./io` validates.
70
+ */
71
+ interface MigrationArtifacts {
72
+ readonly opsJson: string;
73
+ readonly metadata: MigrationMetadata;
74
+ readonly metadataJson: string;
75
+ }
76
+ /**
77
+ * Pure conversion from a `Migration` instance (plus the previously
78
+ * scaffolded metadata, when one exists on disk) to the in-memory
79
+ * artifacts that downstream tooling persists. Owns metadata validation,
80
+ * metadata synthesis/preservation, hint normalization, and the
81
+ * content-addressed `migrationHash` computation, but performs no file I/O
82
+ * — callers handle reads (to source `existing`) and writes (to persist
83
+ * `opsJson` / `metadataJson`).
84
+ */
85
+ declare function buildMigrationArtifacts(instance: Migration, existing: Partial<MigrationMetadata> | null): MigrationArtifacts;
55
86
  //#endregion
56
- export { Migration, type MigrationMeta };
87
+ export { Migration, type MigrationArtifacts, type MigrationMeta, buildMigrationArtifacts, isDirectEntrypoint };
57
88
  //# 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":";;;;UAiBiB,aAAA;;EAAA,SAAA,EAAA,EAAA,MAAa;EA0BR,SAAA,MAAS,CAAA,EAAA,SAAA,MAAA,EAAA;;;;;;;;;;;AAIlB,uBAJS,SAIT,CAAA,mBAHQ,sBAGR,GAHiC,sBAGjC,EAAA,kBAAA,MAAA,GAAA,MAAA,EAAA,kBAAA,MAAA,GAAA,MAAA,CAAA,YAAA,aAAA,CAAA;EAAa,kBAAA,QAAA,EAAA,MAAA;EAmDV;AAsBhB;AAsHA;;;;;;;4BAlL4B,aAAa,WAAW;sBAE9B,aAAa,WAAW;;;;;;;sCAUR;;;;;;uBAOf;;;;;;;;;;;;;;;iBAmBP,kBAAA;;;;;;;;;;;;UAsBC,kBAAA;;qBAEI;;;;;;;;;;;;iBAoHL,uBAAA,WACJ,qBACA,QAAQ,4BACjB"}
@@ -1,16 +1,16 @@
1
- import "../io-CCnYsUHU.mjs";
2
- import { t as computeMigrationId } from "../attestation-DtF8tEOM.mjs";
3
- import { type } from "arktype";
4
- import { dirname, join } from "pathe";
1
+ import { S as errorStaleContractBookends, u as errorInvalidOperationEntry } from "../errors-CfmjBeK0.mjs";
2
+ import { t as computeMigrationHash } from "../hash-BARZdVgW.mjs";
3
+ import { t as deriveProvidedInvariants } from "../invariants-30VA65sB.mjs";
4
+ import { t as MigrationOpSchema } from "../op-schema-DZKFua46.mjs";
5
5
  import { ifDefined } from "@prisma-next/utils/defined";
6
- import { readFileSync, realpathSync, writeFileSync } from "node:fs";
6
+ import { type } from "arktype";
7
+ import { realpathSync } from "node:fs";
7
8
  import { fileURLToPath } from "node:url";
8
9
 
9
10
  //#region src/migration-base.ts
10
11
  const MigrationMetaSchema = type({
11
- from: "string",
12
+ from: "string > 0 | null",
12
13
  to: "string",
13
- "kind?": "'regular' | 'baseline'",
14
14
  "labels?": type("string").array()
15
15
  });
16
16
  /**
@@ -18,106 +18,123 @@ const MigrationMetaSchema = type({
18
18
  *
19
19
  * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
20
20
  * runner can consume it directly via `targetId`, `operations`, `origin`, and
21
- * `destination`. The manifest-shaped inputs come from `describe()`, which
21
+ * `destination`. The metadata-shaped inputs come from `describe()`, which
22
22
  * every migration must implement — `migration.json` is required for a
23
23
  * migration to be valid.
24
24
  */
25
25
  var Migration = class {
26
+ /**
27
+ * Assembled `ControlStack` injected by the orchestrator (`runMigration`).
28
+ *
29
+ * Subclasses (e.g. `PostgresMigration`) read the stack to materialize their
30
+ * adapter once per instance. Optional at the abstract level so unit tests can
31
+ * construct `Migration` instances purely for `operations` / `describe`
32
+ * assertions without needing a real stack; concrete subclasses that need the
33
+ * stack at runtime should narrow the parameter to required.
34
+ */
35
+ stack;
36
+ constructor(stack) {
37
+ this.stack = stack;
38
+ }
26
39
  get origin() {
27
40
  const from = this.describe().from;
28
- return from === "" ? null : { storageHash: from };
41
+ return from === null ? null : { storageHash: from };
29
42
  }
30
43
  get destination() {
31
44
  return { storageHash: this.describe().to };
32
45
  }
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
46
  };
73
- function printHelp() {
74
- process.stdout.write([
75
- "Usage: node <migration-file> [options]",
76
- "",
77
- "Options:",
78
- " --dry-run Print operations to stdout without writing files",
79
- " --help Show this help message",
80
- ""
81
- ].join("\n"));
47
+ /**
48
+ * Returns true when `import.meta.url` resolves to the same file that was
49
+ * invoked as the node entrypoint (`process.argv[1]`). Used by
50
+ * `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when
51
+ * the migration module is being imported (e.g. by another script) rather
52
+ * than executed directly.
53
+ */
54
+ function isDirectEntrypoint(importMetaUrl) {
55
+ const metaFilename = fileURLToPath(importMetaUrl);
56
+ const argv1 = process.argv[1];
57
+ if (!argv1) return false;
58
+ try {
59
+ return realpathSync(metaFilename) === realpathSync(argv1);
60
+ } catch {
61
+ return false;
62
+ }
82
63
  }
83
64
  /**
84
- * Build the attested manifest written by `Migration.run()`.
65
+ * Build the attested metadata from `describe()`-derived metadata, the
66
+ * operations list, and the previously-scaffolded metadata (if any).
85
67
  *
86
- * When a `migration.json` already exists in the directory (the common case:
87
- * the package was scaffolded by `migration plan`), preserve the contract
68
+ * When a `migration.json` already exists for this package (the common
69
+ * case: it was scaffolded by `migration plan`), preserve the contract
88
70
  * bookends, hints, labels, and `createdAt` set there — those fields are
89
71
  * owned by the CLI scaffolder, not the authored class. Only the
90
- * `describe()`-derived fields (`from`, `to`, `kind`) and the operations
91
- * change as the author iterates. When no manifest exists yet (a bare
72
+ * `describe()`-derived fields (`from`, `to`) and the operations
73
+ * change as the author iterates. When no metadata exists yet (a bare
92
74
  * `migration.ts` run from scratch), synthesize a minimal but
93
- * schema-conformant manifest so the resulting package can still be read,
75
+ * schema-conformant record so the resulting package can still be read,
94
76
  * verified, and applied.
95
77
  *
96
- * The `migrationId` is recomputed against the current manifest + ops so
78
+ * The `migrationHash` is recomputed against the current metadata + ops so
97
79
  * the on-disk artifacts are always fully attested.
98
80
  */
99
- function buildAttestedManifest(migrationDir, meta, ops) {
100
- const existing = readExistingManifest(join(migrationDir, "migration.json"));
101
- const baseManifest = {
81
+ function buildAttestedMetadata(meta, ops, existing) {
82
+ assertBookendsMatchMeta(meta, existing);
83
+ const baseMetadata = {
102
84
  from: meta.from,
103
85
  to: meta.to,
104
- kind: meta.kind ?? "regular",
105
86
  labels: meta.labels ?? existing?.labels ?? [],
87
+ providedInvariants: deriveProvidedInvariants(ops),
106
88
  createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
107
89
  fromContract: existing?.fromContract ?? null,
108
90
  toContract: existing?.toContract ?? { storage: { storageHash: meta.to } },
109
91
  hints: normalizeHints(existing?.hints),
110
92
  ...ifDefined("authorship", existing?.authorship)
111
93
  };
112
- const migrationId = computeMigrationId(baseManifest, ops);
94
+ const migrationHash = computeMigrationHash(baseMetadata, ops);
113
95
  return {
114
- ...baseManifest,
115
- migrationId
96
+ ...baseMetadata,
97
+ migrationHash
116
98
  };
117
99
  }
118
100
  /**
101
+ * Verify each preserved contract bookend in `existing` agrees with the
102
+ * corresponding side of `describe()`'s output. A mismatch indicates the
103
+ * migration's `describe()` was edited after `migration plan` scaffolded
104
+ * the package, leaving a self-inconsistent manifest. Failing fast at
105
+ * write-time turns a silent foot-gun into an actionable diagnostic.
106
+ *
107
+ * Skipped when a side's `existing.<side>Contract` is null/absent (the
108
+ * synthesis path stays open for origin-less initial migrations and for
109
+ * bare `migration.ts` runs from scratch). When a bookend is *present*
110
+ * but its `storage.storageHash` is missing, that's treated as a
111
+ * mismatch — a malformed bookend is not equivalent to "no bookend".
112
+ *
113
+ * This check is paired with TML-2274, which removes `fromContract` /
114
+ * `toContract` from the manifest entirely; once that lands, this
115
+ * function and its error code are deleted.
116
+ */
117
+ function assertBookendsMatchMeta(meta, existing) {
118
+ if (existing?.fromContract != null) {
119
+ const contractHash = existing.fromContract.storage?.storageHash ?? "";
120
+ if (contractHash !== meta.from) throw errorStaleContractBookends({
121
+ side: "from",
122
+ metaHash: meta.from,
123
+ contractHash
124
+ });
125
+ }
126
+ if (existing?.toContract != null) {
127
+ const contractHash = existing.toContract.storage?.storageHash ?? "";
128
+ if (contractHash !== meta.to) throw errorStaleContractBookends({
129
+ side: "to",
130
+ metaHash: meta.to,
131
+ contractHash
132
+ });
133
+ }
134
+ }
135
+ /**
119
136
  * Project `existing.hints` down to the known `MigrationHints` shape, dropping
120
- * any legacy keys that may linger in manifests scaffolded by older CLI
137
+ * any legacy keys that may linger in metadata scaffolded by older CLI
121
138
  * versions (e.g. `planningStrategy`). Picking fields explicitly instead of
122
139
  * spreading keeps refreshed `migration.json` files schema-clean regardless
123
140
  * of what was on disk before.
@@ -129,38 +146,32 @@ function normalizeHints(existing) {
129
146
  plannerVersion: existing?.plannerVersion ?? "2.0.0"
130
147
  };
131
148
  }
132
- function readExistingManifest(manifestPath) {
133
- let raw;
134
- try {
135
- raw = readFileSync(manifestPath, "utf-8");
136
- } catch {
137
- return null;
138
- }
139
- try {
140
- return JSON.parse(raw);
141
- } catch {
142
- return null;
143
- }
144
- }
145
- function serializeMigration(MigrationClass, migrationDir, dryRun) {
146
- const instance = new MigrationClass();
149
+ /**
150
+ * Pure conversion from a `Migration` instance (plus the previously
151
+ * scaffolded metadata, when one exists on disk) to the in-memory
152
+ * artifacts that downstream tooling persists. Owns metadata validation,
153
+ * metadata synthesis/preservation, hint normalization, and the
154
+ * content-addressed `migrationHash` computation, but performs no file I/O
155
+ * — callers handle reads (to source `existing`) and writes (to persist
156
+ * `opsJson` / `metadataJson`).
157
+ */
158
+ function buildMigrationArtifacts(instance, existing) {
147
159
  const ops = instance.operations;
148
160
  if (!Array.isArray(ops)) throw new Error("operations must be an array");
149
- const serializedOps = JSON.stringify(ops, null, 2);
161
+ for (let index = 0; index < ops.length; index++) {
162
+ const result = MigrationOpSchema(ops[index]);
163
+ if (result instanceof type.errors) throw errorInvalidOperationEntry(index, result.summary);
164
+ }
150
165
  const parsed = MigrationMetaSchema(instance.describe());
151
166
  if (parsed instanceof type.errors) throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
152
- const manifest = buildAttestedManifest(migrationDir, parsed, ops);
153
- if (dryRun) {
154
- process.stdout.write(`--- migration.json ---\n${JSON.stringify(manifest, null, 2)}\n`);
155
- process.stdout.write("--- ops.json ---\n");
156
- process.stdout.write(`${serializedOps}\n`);
157
- return;
158
- }
159
- writeFileSync(join(migrationDir, "ops.json"), serializedOps);
160
- writeFileSync(join(migrationDir, "migration.json"), JSON.stringify(manifest, null, 2));
161
- process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\n`);
167
+ const metadata = buildAttestedMetadata(parsed, ops, existing);
168
+ return {
169
+ opsJson: JSON.stringify(ops, null, 2),
170
+ metadata,
171
+ metadataJson: JSON.stringify(metadata, null, 2)
172
+ };
162
173
  }
163
174
 
164
175
  //#endregion
165
- export { Migration };
176
+ export { Migration, buildMigrationArtifacts, isDirectEntrypoint };
166
177
  //# sourceMappingURL=migration.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"migration.mjs","names":["isEntrypoint: boolean","baseManifest: Omit<MigrationManifest, 'migrationId'>","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 { 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<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 * The `migrationId` is recomputed against the current manifest + ops so\n * the on-disk artifacts are always fully attested.\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: 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\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;;;;;;;;;;;;;;;;;;AAmBH,SAAS,sBACP,cACA,MACA,KACmB;CACnB,MAAM,WAAW,qBAAqB,KAAK,cAAc,iBAAiB,CAAC;CAE3E,MAAMC,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;;AAGH,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":["baseMetadata: Omit<MigrationMetadata, 'migrationHash'>"],"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 { errorInvalidOperationEntry, errorStaleContractBookends } from './errors';\nimport { computeMigrationHash } from './hash';\nimport { deriveProvidedInvariants } from './invariants';\nimport type { MigrationHints, MigrationMetadata } from './metadata';\nimport { MigrationOpSchema } from './op-schema';\nimport type { MigrationOps } from './package';\n\nexport interface MigrationMeta {\n readonly from: string | null;\n readonly to: string;\n readonly labels?: readonly string[];\n}\n\n// `from` rejects empty strings to mirror `MigrationMetadataSchema` in\n// `./io.ts`. Without this match, an authored migration could `describe()` with\n// `from: ''` and pass `buildMigrationArtifacts`'s validation, only to have\n// `readMigrationPackage` reject the resulting `migration.json` later — the\n// two validators must agree on the legal value space.\nconst MigrationMetaSchema = type({\n from: 'string > 0 | null',\n to: 'string',\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 metadata-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 return from === null ? 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\n/**\n * In-memory artifacts produced from a `Migration` instance: the\n * serialized `ops.json` body, the `migration.json` metadata 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 *\n * `metadataJson` is `JSON.stringify(metadata, null, 2)` — the canonical\n * on-disk shape that the arktype loader-schema in `./io` validates.\n */\nexport interface MigrationArtifacts {\n readonly opsJson: string;\n readonly metadata: MigrationMetadata;\n readonly metadataJson: string;\n}\n\n/**\n * Build the attested metadata from `describe()`-derived metadata, the\n * operations list, and the previously-scaffolded metadata (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`) and the operations\n * change as the author iterates. When no metadata exists yet (a bare\n * `migration.ts` run from scratch), synthesize a minimal but\n * schema-conformant record so the resulting package can still be read,\n * verified, and applied.\n *\n * The `migrationHash` is recomputed against the current metadata + ops so\n * the on-disk artifacts are always fully attested.\n */\nfunction buildAttestedMetadata(\n meta: MigrationMeta,\n ops: MigrationOps,\n existing: Partial<MigrationMetadata> | null,\n): MigrationMetadata {\n assertBookendsMatchMeta(meta, existing);\n\n const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {\n from: meta.from,\n to: meta.to,\n labels: meta.labels ?? existing?.labels ?? [],\n providedInvariants: deriveProvidedInvariants(ops),\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n fromContract: existing?.fromContract ?? null,\n // When no scaffolded metadata 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 `computeMigrationHash`), 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 migrationHash = computeMigrationHash(baseMetadata, ops);\n return { ...baseMetadata, migrationHash };\n}\n\n/**\n * Verify each preserved contract bookend in `existing` agrees with the\n * corresponding side of `describe()`'s output. A mismatch indicates the\n * migration's `describe()` was edited after `migration plan` scaffolded\n * the package, leaving a self-inconsistent manifest. Failing fast at\n * write-time turns a silent foot-gun into an actionable diagnostic.\n *\n * Skipped when a side's `existing.<side>Contract` is null/absent (the\n * synthesis path stays open for origin-less initial migrations and for\n * bare `migration.ts` runs from scratch). When a bookend is *present*\n * but its `storage.storageHash` is missing, that's treated as a\n * mismatch — a malformed bookend is not equivalent to \"no bookend\".\n *\n * This check is paired with TML-2274, which removes `fromContract` /\n * `toContract` from the manifest entirely; once that lands, this\n * function and its error code are deleted.\n */\nfunction assertBookendsMatchMeta(\n meta: MigrationMeta,\n existing: Partial<MigrationMetadata> | null,\n): void {\n if (existing?.fromContract != null) {\n const contractHash = existing.fromContract.storage?.storageHash ?? '';\n if (contractHash !== meta.from) {\n throw errorStaleContractBookends({\n side: 'from',\n metaHash: meta.from,\n contractHash,\n });\n }\n }\n if (existing?.toContract != null) {\n const contractHash = existing.toContract.storage?.storageHash ?? '';\n if (contractHash !== meta.to) {\n throw errorStaleContractBookends({\n side: 'to',\n metaHash: meta.to,\n contractHash,\n });\n }\n }\n}\n\n/**\n * Project `existing.hints` down to the known `MigrationHints` shape, dropping\n * any legacy keys that may linger in metadata 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 metadata, when one exists on disk) to the in-memory\n * artifacts that downstream tooling persists. Owns metadata validation,\n * metadata synthesis/preservation, hint normalization, and the\n * content-addressed `migrationHash` computation, but performs no file I/O\n * — callers handle reads (to source `existing`) and writes (to persist\n * `opsJson` / `metadataJson`).\n */\nexport function buildMigrationArtifacts(\n instance: Migration,\n existing: Partial<MigrationMetadata> | 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 for (let index = 0; index < ops.length; index++) {\n const result = MigrationOpSchema(ops[index]);\n if (result instanceof type.errors) {\n throw errorInvalidOperationEntry(index, result.summary);\n }\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 metadata = buildAttestedMetadata(parsed, ops, existing);\n\n return {\n opsJson: JSON.stringify(ops, null, 2),\n metadata,\n metadataJson: JSON.stringify(metadata, null, 2),\n };\n}\n"],"mappings":";;;;;;;;;;AA4BA,MAAM,sBAAsB,KAAK;CAC/B,MAAM;CACN,IAAI;CACJ,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;AAC7B,SAAO,SAAS,OAAO,OAAO,EAAE,aAAa,MAAM;;CAGrD,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;;;;;;;;;;;;;;;;;;;;AAsCX,SAAS,sBACP,MACA,KACA,UACmB;AACnB,yBAAwB,MAAM,SAAS;CAEvC,MAAMA,eAAyD;EAC7D,MAAM,KAAK;EACX,IAAI,KAAK;EACT,QAAQ,KAAK,UAAU,UAAU,UAAU,EAAE;EAC7C,oBAAoB,yBAAyB,IAAI;EACjD,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,gBAAgB,qBAAqB,cAAc,IAAI;AAC7D,QAAO;EAAE,GAAG;EAAc;EAAe;;;;;;;;;;;;;;;;;;;AAoB3C,SAAS,wBACP,MACA,UACM;AACN,KAAI,UAAU,gBAAgB,MAAM;EAClC,MAAM,eAAe,SAAS,aAAa,SAAS,eAAe;AACnE,MAAI,iBAAiB,KAAK,KACxB,OAAM,2BAA2B;GAC/B,MAAM;GACN,UAAU,KAAK;GACf;GACD,CAAC;;AAGN,KAAI,UAAU,cAAc,MAAM;EAChC,MAAM,eAAe,SAAS,WAAW,SAAS,eAAe;AACjE,MAAI,iBAAiB,KAAK,GACxB,OAAM,2BAA2B;GAC/B,MAAM;GACN,UAAU,KAAK;GACf;GACD,CAAC;;;;;;;;;;AAYR,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;AAGhD,MAAK,IAAI,QAAQ,GAAG,QAAQ,IAAI,QAAQ,SAAS;EAC/C,MAAM,SAAS,kBAAkB,IAAI,OAAO;AAC5C,MAAI,kBAAkB,KAAK,OACzB,OAAM,2BAA2B,OAAO,OAAO,QAAQ;;CAK3D,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"}
@@ -0,0 +1,2 @@
1
+ import { n as MigrationPackage, t as MigrationOps } from "../package-5HCCg0z-.mjs";
2
+ export { type MigrationOps, type MigrationPackage };
@@ -0,0 +1 @@
1
+ export { };
@@ -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"}
@@ -1,7 +1,7 @@
1
- import { c as errorInvalidRefName, l as errorInvalidRefValue, t as MigrationToolsError, u as errorInvalidRefs } from "../errors-BKbRGCJM.mjs";
2
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
1
+ import { d as errorInvalidRefFile, f as errorInvalidRefName, p as errorInvalidRefValue, t as MigrationToolsError } from "../errors-CfmjBeK0.mjs";
2
+ import { dirname, join, relative } from "pathe";
3
+ import { mkdir, readFile, readdir, rename, rmdir, unlink, writeFile } from "node:fs/promises";
3
4
  import { type } from "arktype";
4
- import { dirname, join } from "pathe";
5
5
 
6
6
  //#region src/refs.ts
7
7
  const REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
@@ -16,58 +16,134 @@ function validateRefName(name) {
16
16
  function validateRefValue(value) {
17
17
  return REF_VALUE_PATTERN.test(value);
18
18
  }
19
- const RefsSchema = type("Record<string, string>").narrow((refs, ctx) => {
20
- for (const [key, value] of Object.entries(refs)) {
21
- if (!validateRefName(key)) return ctx.mustBe(`valid ref names (invalid: "${key}")`);
22
- if (!validateRefValue(value)) return ctx.mustBe(`valid contract hashes (invalid value for "${key}": "${value}")`);
23
- }
19
+ const RefEntrySchema = type({
20
+ hash: "string",
21
+ invariants: "string[]"
22
+ }).narrow((entry, ctx) => {
23
+ if (!validateRefValue(entry.hash)) return ctx.mustBe(`a valid contract hash (got "${entry.hash}")`);
24
24
  return true;
25
25
  });
26
- async function readRefs(refsPath) {
26
+ function refFilePath(refsDir, name) {
27
+ return join(refsDir, `${name}.json`);
28
+ }
29
+ function refNameFromPath(refsDir, filePath) {
30
+ return relative(refsDir, filePath).replace(/\.json$/, "");
31
+ }
32
+ async function readRef(refsDir, name) {
33
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
34
+ const filePath = refFilePath(refsDir, name);
27
35
  let raw;
28
36
  try {
29
- raw = await readFile(refsPath, "utf-8");
37
+ raw = await readFile(filePath, "utf-8");
30
38
  } catch (error) {
31
- if (error instanceof Error && error.code === "ENOENT") return {};
39
+ if (error instanceof Error && error.code === "ENOENT") throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
40
+ why: `No ref file found at "${filePath}".`,
41
+ fix: `Create the ref with: prisma-next migration ref set ${name} <hash>`,
42
+ details: {
43
+ refName: name,
44
+ filePath
45
+ }
46
+ });
32
47
  throw error;
33
48
  }
34
49
  let parsed;
35
50
  try {
36
51
  parsed = JSON.parse(raw);
37
52
  } catch {
38
- throw errorInvalidRefs(refsPath, "Failed to parse as JSON");
53
+ throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
39
54
  }
40
- const result = RefsSchema(parsed);
41
- if (result instanceof type.errors) throw errorInvalidRefs(refsPath, result.summary);
55
+ const result = RefEntrySchema(parsed);
56
+ if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
42
57
  return result;
43
58
  }
44
- async function writeRefs(refsPath, refs) {
45
- for (const [key, value] of Object.entries(refs)) {
46
- if (!validateRefName(key)) throw errorInvalidRefName(key);
47
- if (!validateRefValue(value)) throw errorInvalidRefValue(value);
59
+ async function readRefs(refsDir) {
60
+ let entries;
61
+ try {
62
+ entries = await readdir(refsDir, {
63
+ recursive: true,
64
+ encoding: "utf-8"
65
+ });
66
+ } catch (error) {
67
+ if (error instanceof Error && error.code === "ENOENT") return {};
68
+ throw error;
69
+ }
70
+ const jsonFiles = entries.filter((entry) => entry.endsWith(".json"));
71
+ const refs = {};
72
+ for (const jsonFile of jsonFiles) {
73
+ const filePath = join(refsDir, jsonFile);
74
+ const name = refNameFromPath(refsDir, filePath);
75
+ let raw;
76
+ try {
77
+ raw = await readFile(filePath, "utf-8");
78
+ } catch (error) {
79
+ const code = error instanceof Error ? error.code : void 0;
80
+ if (code === "ENOENT" || code === "EISDIR") continue;
81
+ throw error;
82
+ }
83
+ let parsed;
84
+ try {
85
+ parsed = JSON.parse(raw);
86
+ } catch {
87
+ throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
88
+ }
89
+ const result = RefEntrySchema(parsed);
90
+ if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
91
+ refs[name] = result;
48
92
  }
49
- const sorted = Object.fromEntries(Object.entries(refs).sort(([a], [b]) => a.localeCompare(b)));
50
- const dir = dirname(refsPath);
93
+ return refs;
94
+ }
95
+ async function writeRef(refsDir, name, entry) {
96
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
97
+ if (!validateRefValue(entry.hash)) throw errorInvalidRefValue(entry.hash);
98
+ const filePath = refFilePath(refsDir, name);
99
+ const dir = dirname(filePath);
51
100
  await mkdir(dir, { recursive: true });
52
- const tmpPath = join(dir, `.refs.json.${Date.now()}.tmp`);
53
- await writeFile(tmpPath, `${JSON.stringify(sorted, null, 2)}\n`);
54
- await rename(tmpPath, refsPath);
101
+ const tmpPath = join(dir, `.${name.split("/").pop()}.json.${Date.now()}.tmp`);
102
+ await writeFile(tmpPath, `${JSON.stringify({
103
+ hash: entry.hash,
104
+ invariants: [...entry.invariants]
105
+ }, null, 2)}\n`);
106
+ await rename(tmpPath, filePath);
107
+ }
108
+ async function deleteRef(refsDir, name) {
109
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
110
+ const filePath = refFilePath(refsDir, name);
111
+ try {
112
+ await unlink(filePath);
113
+ } catch (error) {
114
+ if (error instanceof Error && error.code === "ENOENT") throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
115
+ why: `No ref file found at "${filePath}".`,
116
+ fix: "Run `prisma-next migration ref list` to see available refs.",
117
+ details: {
118
+ refName: name,
119
+ filePath
120
+ }
121
+ });
122
+ throw error;
123
+ }
124
+ let dir = dirname(filePath);
125
+ while (dir !== refsDir && dir.startsWith(refsDir)) try {
126
+ await rmdir(dir);
127
+ dir = dirname(dir);
128
+ } catch (error) {
129
+ const code = error instanceof Error ? error.code : void 0;
130
+ if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOENT") break;
131
+ throw error;
132
+ }
55
133
  }
56
134
  function resolveRef(refs, name) {
57
135
  if (!validateRefName(name)) throw errorInvalidRefName(name);
58
- const hash = refs[name];
59
- if (hash === void 0) throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
60
- why: `No ref named "${name}" exists in refs.json.`,
61
- fix: `Available refs: ${Object.keys(refs).join(", ") || "(none)"}. Create a ref with: set the "${name}" key in migrations/refs.json.`,
136
+ if (!Object.hasOwn(refs, name)) throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
137
+ why: `No ref named "${name}" exists.`,
138
+ fix: `Available refs: ${Object.keys(refs).join(", ") || "(none)"}. Create a ref with: prisma-next migration ref set ${name} <hash>`,
62
139
  details: {
63
140
  refName: name,
64
141
  availableRefs: Object.keys(refs)
65
142
  }
66
143
  });
67
- if (!validateRefValue(hash)) throw errorInvalidRefValue(hash);
68
- return hash;
144
+ return refs[name];
69
145
  }
70
146
 
71
147
  //#endregion
72
- export { readRefs, resolveRef, validateRefName, validateRefValue, writeRefs };
148
+ export { deleteRef, readRef, readRefs, resolveRef, validateRefName, validateRefValue, writeRef };
73
149
  //# sourceMappingURL=refs.mjs.map