@prisma-next/migration-tools 0.4.0-dev.9 → 0.5.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{attestation-DnebS4XZ.mjs → attestation-DtF8tEOM.mjs} +24 -23
- package/dist/attestation-DtF8tEOM.mjs.map +1 -0
- package/dist/{errors-C_XuSbX7.mjs → errors-BKbRGCJM.mjs} +9 -2
- package/dist/errors-BKbRGCJM.mjs.map +1 -0
- package/dist/exports/attestation.d.mts +20 -6
- package/dist/exports/attestation.d.mts.map +1 -1
- package/dist/exports/attestation.mjs +3 -3
- package/dist/exports/dag.d.mts +8 -6
- package/dist/exports/dag.d.mts.map +1 -1
- package/dist/exports/dag.mjs +181 -107
- package/dist/exports/dag.mjs.map +1 -1
- package/dist/exports/io.d.mts +16 -13
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +2 -2
- package/dist/exports/migration-ts.d.mts +15 -21
- package/dist/exports/migration-ts.d.mts.map +1 -1
- package/dist/exports/migration-ts.mjs +28 -36
- package/dist/exports/migration-ts.mjs.map +1 -1
- package/dist/exports/migration.d.mts +48 -18
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +75 -85
- package/dist/exports/migration.mjs.map +1 -1
- package/dist/exports/refs.mjs +1 -1
- package/dist/exports/types.d.mts +2 -2
- package/dist/exports/types.mjs +2 -16
- package/dist/{io-Cun81AIZ.mjs → io-CCnYsUHU.mjs} +18 -22
- package/dist/io-CCnYsUHU.mjs.map +1 -0
- package/dist/types-DyGXcWWp.d.mts +71 -0
- package/dist/types-DyGXcWWp.d.mts.map +1 -0
- package/package.json +5 -4
- package/src/attestation.ts +34 -26
- package/src/dag.ts +140 -154
- package/src/errors.ts +8 -0
- package/src/exports/attestation.ts +2 -1
- package/src/exports/io.ts +1 -1
- package/src/exports/migration-ts.ts +1 -1
- package/src/exports/migration.ts +8 -1
- package/src/exports/types.ts +2 -8
- package/src/graph-ops.ts +65 -0
- package/src/io.ts +23 -24
- package/src/migration-base.ts +99 -101
- package/src/migration-ts.ts +28 -50
- package/src/queue.ts +37 -0
- package/src/types.ts +15 -55
- package/dist/attestation-DnebS4XZ.mjs.map +0 -1
- package/dist/errors-C_XuSbX7.mjs.map +0 -1
- package/dist/exports/types.mjs.map +0 -1
- package/dist/io-Cun81AIZ.mjs.map +0 -1
- package/dist/types-D2uX4ql7.d.mts +0 -100
- package/dist/types-D2uX4ql7.d.mts.map +0 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import "../io-
|
|
2
|
-
import {
|
|
1
|
+
import "../io-CCnYsUHU.mjs";
|
|
2
|
+
import { t as computeMigrationId } from "../attestation-DtF8tEOM.mjs";
|
|
3
3
|
import { type } from "arktype";
|
|
4
|
-
import { dirname, join } from "pathe";
|
|
5
4
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
6
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
87
|
-
*
|
|
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
|
-
*
|
|
97
|
-
*
|
|
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(
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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":"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"}
|
|
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"}
|
package/dist/exports/refs.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as
|
|
1
|
+
import { c as errorInvalidRefName, l as errorInvalidRefValue, t as MigrationToolsError, u as errorInvalidRefs } from "../errors-BKbRGCJM.mjs";
|
|
2
2
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { type } from "arktype";
|
|
4
4
|
import { dirname, join } from "pathe";
|
package/dist/exports/types.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as MigrationManifest, i as MigrationHints, n as MigrationChainEntry, o as MigrationOps, r as MigrationGraph, t as MigrationBundle } from "../types-DyGXcWWp.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/errors.d.ts
|
|
4
4
|
|
|
@@ -31,5 +31,5 @@ declare class MigrationToolsError extends Error {
|
|
|
31
31
|
static is(error: unknown): error is MigrationToolsError;
|
|
32
32
|
}
|
|
33
33
|
//#endregion
|
|
34
|
-
export { type
|
|
34
|
+
export { type MigrationBundle, type MigrationBundle as MigrationPackage, type MigrationChainEntry, type MigrationGraph, type MigrationHints, type MigrationManifest, type MigrationOps, MigrationToolsError };
|
|
35
35
|
//# sourceMappingURL=types.d.mts.map
|
package/dist/exports/types.mjs
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
import { t as MigrationToolsError } from "../errors-
|
|
1
|
+
import { t as MigrationToolsError } from "../errors-BKbRGCJM.mjs";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Type guard that narrows a MigrationBundle to an AttestedMigrationBundle.
|
|
6
|
-
* Use with `.filter(isAttested)` to get a typed array of attested bundles.
|
|
7
|
-
*/
|
|
8
|
-
function isAttested(bundle) {
|
|
9
|
-
return typeof bundle.manifest.migrationId === "string";
|
|
10
|
-
}
|
|
11
|
-
function isDraft(bundle) {
|
|
12
|
-
return bundle.manifest.migrationId === null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
//#endregion
|
|
16
|
-
export { MigrationToolsError, isAttested, isDraft };
|
|
17
|
-
//# sourceMappingURL=types.mjs.map
|
|
3
|
+
export { MigrationToolsError };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as errorInvalidDestName, d as errorInvalidSlug, f as errorMissingFile, o as errorInvalidJson, r as errorDirectoryExists, s as errorInvalidManifest } from "./errors-BKbRGCJM.mjs";
|
|
2
2
|
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { type } from "arktype";
|
|
4
4
|
import { basename, dirname, join } from "pathe";
|
|
@@ -13,15 +13,14 @@ function hasErrnoCode(error, code) {
|
|
|
13
13
|
const MigrationManifestSchema = type({
|
|
14
14
|
from: "string",
|
|
15
15
|
to: "string",
|
|
16
|
-
migrationId: "string
|
|
16
|
+
migrationId: "string",
|
|
17
17
|
kind: "'regular' | 'baseline'",
|
|
18
18
|
fromContract: "object | null",
|
|
19
19
|
toContract: "object",
|
|
20
20
|
hints: type({
|
|
21
21
|
used: "string[]",
|
|
22
22
|
applied: "string[]",
|
|
23
|
-
plannerVersion: "string"
|
|
24
|
-
planningStrategy: "string"
|
|
23
|
+
plannerVersion: "string"
|
|
25
24
|
}),
|
|
26
25
|
labels: "string[]",
|
|
27
26
|
"authorship?": type({
|
|
@@ -51,24 +50,21 @@ async function writeMigrationPackage(dir, manifest, ops) {
|
|
|
51
50
|
await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: "wx" });
|
|
52
51
|
}
|
|
53
52
|
/**
|
|
54
|
-
* Copy
|
|
55
|
-
* colocated `contract.d.ts`) into the migration package directory so
|
|
56
|
-
* authors of the scaffolded `migration.ts` can import the typed
|
|
57
|
-
* contract relative to the migration directory
|
|
58
|
-
* (`import type { Contract } from './contract'`).
|
|
53
|
+
* Copy a list of files into `destDir`, optionally renaming each one.
|
|
59
54
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
55
|
+
* The destination directory is created (with `recursive: true`) if it
|
|
56
|
+
* does not already exist. Each source path is copied byte-for-byte into
|
|
57
|
+
* `destDir/<destName>`; missing sources throw `ENOENT`. The helper is
|
|
58
|
+
* intentionally generic: callers own the list of files (e.g. a contract
|
|
59
|
+
* emitter's emitted output) and the naming convention (e.g. renaming
|
|
60
|
+
* the destination contract to `end-contract.*` and the source contract
|
|
61
|
+
* to `start-contract.*`).
|
|
63
62
|
*/
|
|
64
|
-
async function
|
|
65
|
-
await
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
await copyFile(
|
|
69
|
-
} catch (error) {
|
|
70
|
-
if (hasErrnoCode(error, "ENOENT")) return;
|
|
71
|
-
throw error;
|
|
63
|
+
async function copyFilesWithRename(destDir, files) {
|
|
64
|
+
await mkdir(destDir, { recursive: true });
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
if (basename(file.destName) !== file.destName) throw errorInvalidDestName(file.destName);
|
|
67
|
+
await copyFile(file.sourcePath, join(destDir, file.destName));
|
|
72
68
|
}
|
|
73
69
|
}
|
|
74
70
|
async function writeMigrationManifest(dir, manifest) {
|
|
@@ -153,5 +149,5 @@ function formatMigrationDirName(timestamp, slug) {
|
|
|
153
149
|
}
|
|
154
150
|
|
|
155
151
|
//#endregion
|
|
156
|
-
export { writeMigrationManifest as a, readMigrationsDir as i, formatMigrationDirName as n, writeMigrationOps as o, readMigrationPackage as r, writeMigrationPackage as s,
|
|
157
|
-
//# sourceMappingURL=io-
|
|
152
|
+
export { writeMigrationManifest as a, readMigrationsDir as i, formatMigrationDirName as n, writeMigrationOps as o, readMigrationPackage as r, writeMigrationPackage as s, copyFilesWithRename as t };
|
|
153
|
+
//# sourceMappingURL=io-CCnYsUHU.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"io-CCnYsUHU.mjs","names":["manifestRaw: string","opsRaw: string","manifest: MigrationManifest","ops: MigrationOps","entries: string[]","packages: MigrationBundle[]"],"sources":["../src/io.ts"],"sourcesContent":["import { copyFile, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport {\n errorDirectoryExists,\n errorInvalidDestName,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMissingFile,\n} from './errors';\nimport type { MigrationBundle, MigrationManifest, MigrationOps } from './types';\n\nconst MANIFEST_FILE = 'migration.json';\nconst OPS_FILE = 'ops.json';\nconst MAX_SLUG_LENGTH = 64;\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nconst MigrationHintsSchema = type({\n used: 'string[]',\n applied: 'string[]',\n plannerVersion: 'string',\n});\n\nconst MigrationManifestSchema = type({\n from: 'string',\n to: 'string',\n migrationId: 'string',\n kind: \"'regular' | 'baseline'\",\n fromContract: 'object | null',\n toContract: 'object',\n hints: MigrationHintsSchema,\n labels: 'string[]',\n 'authorship?': type({\n 'author?': 'string',\n 'email?': 'string',\n }),\n 'signature?': type({\n keyId: 'string',\n value: 'string',\n }).or('null'),\n createdAt: 'string',\n});\n\nconst MigrationOpSchema = type({\n id: 'string',\n label: 'string',\n operationClass: \"'additive' | 'widening' | 'destructive' | 'data'\",\n});\n\n// Intentionally shallow: operation-specific payload validation is owned by planner/runner layers.\nconst MigrationOpsSchema = MigrationOpSchema.array();\n\nexport async function writeMigrationPackage(\n dir: string,\n manifest: MigrationManifest,\n ops: MigrationOps,\n): Promise<void> {\n await mkdir(dirname(dir), { recursive: true });\n\n try {\n await mkdir(dir);\n } catch (error) {\n if (hasErrnoCode(error, 'EEXIST')) {\n throw errorDirectoryExists(dir);\n }\n throw error;\n }\n\n await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2), { flag: 'wx' });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\n/**\n * Copy a list of files into `destDir`, optionally renaming each one.\n *\n * The destination directory is created (with `recursive: true`) if it\n * does not already exist. Each source path is copied byte-for-byte into\n * `destDir/<destName>`; missing sources throw `ENOENT`. The helper is\n * intentionally generic: callers own the list of files (e.g. a contract\n * emitter's emitted output) and the naming convention (e.g. renaming\n * the destination contract to `end-contract.*` and the source contract\n * to `start-contract.*`).\n */\nexport async function copyFilesWithRename(\n destDir: string,\n files: readonly { readonly sourcePath: string; readonly destName: string }[],\n): Promise<void> {\n await mkdir(destDir, { recursive: true });\n for (const file of files) {\n if (basename(file.destName) !== file.destName) {\n throw errorInvalidDestName(file.destName);\n }\n await copyFile(file.sourcePath, join(destDir, file.destName));\n }\n}\n\nexport async function writeMigrationManifest(\n dir: string,\n manifest: MigrationManifest,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\\n`);\n}\n\nexport async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {\n await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\\n`);\n}\n\nexport async function readMigrationPackage(dir: string): Promise<MigrationBundle> {\n const manifestPath = join(dir, MANIFEST_FILE);\n const opsPath = join(dir, OPS_FILE);\n\n let manifestRaw: string;\n try {\n manifestRaw = await readFile(manifestPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(MANIFEST_FILE, dir);\n }\n throw error;\n }\n\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(OPS_FILE, dir);\n }\n throw error;\n }\n\n let manifest: MigrationManifest;\n try {\n manifest = JSON.parse(manifestRaw);\n } catch (e) {\n throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));\n }\n\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch (e) {\n throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));\n }\n\n validateManifest(manifest, manifestPath);\n validateOps(ops, opsPath);\n\n return {\n dirName: basename(dir),\n dirPath: dir,\n manifest,\n ops,\n };\n}\n\nfunction validateManifest(\n manifest: unknown,\n filePath: string,\n): asserts manifest is MigrationManifest {\n const result = MigrationManifestSchema(manifest);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nfunction validateOps(ops: unknown, filePath: string): asserts ops is MigrationOps {\n const result = MigrationOpsSchema(ops);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nexport async function readMigrationsDir(\n migrationsRoot: string,\n): Promise<readonly MigrationBundle[]> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const packages: MigrationBundle[] = [];\n\n for (const entry of entries.sort()) {\n const entryPath = join(migrationsRoot, entry);\n const entryStat = await stat(entryPath);\n if (!entryStat.isDirectory()) continue;\n\n const manifestPath = join(entryPath, MANIFEST_FILE);\n try {\n await stat(manifestPath);\n } catch {\n continue; // skip non-migration directories\n }\n\n packages.push(await readMigrationPackage(entryPath));\n }\n\n return packages;\n}\n\nexport function formatMigrationDirName(timestamp: Date, slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n\n if (sanitized.length === 0) {\n throw errorInvalidSlug(slug);\n }\n\n const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);\n\n const y = timestamp.getUTCFullYear();\n const mo = String(timestamp.getUTCMonth() + 1).padStart(2, '0');\n const d = String(timestamp.getUTCDate()).padStart(2, '0');\n const h = String(timestamp.getUTCHours()).padStart(2, '0');\n const mi = String(timestamp.getUTCMinutes()).padStart(2, '0');\n\n return `${y}${mo}${d}T${h}${mi}_${truncated}`;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Contract } from "@prisma-next/contract/types";
|
|
2
|
+
import { MigrationPlanOperation } from "@prisma-next/framework-components/control";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
interface MigrationHints {
|
|
6
|
+
readonly used: readonly string[];
|
|
7
|
+
readonly applied: readonly string[];
|
|
8
|
+
readonly plannerVersion: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* On-disk migration manifest. Every migration is content-addressed: the
|
|
12
|
+
* `migrationId` is a hash over the manifest envelope plus the operations
|
|
13
|
+
* list, computed at write time. There is no draft state — a migration
|
|
14
|
+
* directory either exists with a fully attested manifest or it does not.
|
|
15
|
+
*
|
|
16
|
+
* When the planner cannot lower an operation because of an unfilled
|
|
17
|
+
* `placeholder(...)` slot, the migration is still written with
|
|
18
|
+
* `migrationId` hashed over `ops: []`. Re-running self-emit after the
|
|
19
|
+
* user fills the placeholder produces a *different* `migrationId`
|
|
20
|
+
* (committed to the real ops); this is intentional.
|
|
21
|
+
*/
|
|
22
|
+
interface MigrationManifest {
|
|
23
|
+
readonly migrationId: string;
|
|
24
|
+
readonly from: string;
|
|
25
|
+
readonly to: string;
|
|
26
|
+
readonly kind: 'regular' | 'baseline';
|
|
27
|
+
readonly fromContract: Contract | null;
|
|
28
|
+
readonly toContract: Contract;
|
|
29
|
+
readonly hints: MigrationHints;
|
|
30
|
+
readonly labels: readonly string[];
|
|
31
|
+
readonly authorship?: {
|
|
32
|
+
readonly author?: string;
|
|
33
|
+
readonly email?: string;
|
|
34
|
+
};
|
|
35
|
+
readonly signature?: {
|
|
36
|
+
readonly keyId: string;
|
|
37
|
+
readonly value: string;
|
|
38
|
+
} | null;
|
|
39
|
+
readonly createdAt: string;
|
|
40
|
+
}
|
|
41
|
+
type MigrationOps = readonly MigrationPlanOperation[];
|
|
42
|
+
/**
|
|
43
|
+
* An on-disk migration directory containing a manifest and operations.
|
|
44
|
+
*/
|
|
45
|
+
interface MigrationBundle {
|
|
46
|
+
readonly dirName: string;
|
|
47
|
+
readonly dirPath: string;
|
|
48
|
+
readonly manifest: MigrationManifest;
|
|
49
|
+
readonly ops: MigrationOps;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* An entry in the migration graph. All on-disk migrations are attested,
|
|
53
|
+
* so `migrationId` is always a string.
|
|
54
|
+
*/
|
|
55
|
+
interface MigrationChainEntry {
|
|
56
|
+
readonly from: string;
|
|
57
|
+
readonly to: string;
|
|
58
|
+
readonly migrationId: string;
|
|
59
|
+
readonly dirName: string;
|
|
60
|
+
readonly createdAt: string;
|
|
61
|
+
readonly labels: readonly string[];
|
|
62
|
+
}
|
|
63
|
+
interface MigrationGraph {
|
|
64
|
+
readonly nodes: ReadonlySet<string>;
|
|
65
|
+
readonly forwardChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;
|
|
66
|
+
readonly reverseChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;
|
|
67
|
+
readonly migrationById: ReadonlyMap<string, MigrationChainEntry>;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { MigrationManifest as a, MigrationHints as i, MigrationChainEntry as n, MigrationOps as o, MigrationGraph as r, MigrationBundle as t };
|
|
71
|
+
//# sourceMappingURL=types-DyGXcWWp.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-DyGXcWWp.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;UAGiB,cAAA;;EAAA,SAAA,OAAA,EAAc,SAAA,MAAA,EAAA;EAkBd,SAAA,cAAiB,EAAA,MAAA;;;;;AAclC;AAKA;AAWA;AASA;;;;;;AAI8C,UA3C7B,iBAAA,CA2C6B;EAApB,SAAA,WAAA,EAAA,MAAA;EAAW,SAAA,IAAA,EAAA,MAAA;;;yBAtCZ;uBACF;kBACL;;;;;;;;;;;;KAON,YAAA,YAAwB;;;;UAKnB,eAAA;;;qBAGI;gBACL;;;;;;UAOC,mBAAA;;;;;;;;UASA,cAAA;kBACC;yBACO,6BAA6B;yBAC7B,6BAA6B;0BAC5B,oBAAoB"}
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/migration-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-dev.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "On-disk migration persistence, attestation, and chain reconstruction for Prisma Next",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"arktype": "^2.1.29",
|
|
9
9
|
"pathe": "^2.0.3",
|
|
10
|
-
"
|
|
11
|
-
"@prisma-next/
|
|
12
|
-
"@prisma-next/utils": "0.
|
|
10
|
+
"prettier": "^3.6.2",
|
|
11
|
+
"@prisma-next/contract": "0.5.0-dev.1",
|
|
12
|
+
"@prisma-next/utils": "0.5.0-dev.1",
|
|
13
|
+
"@prisma-next/framework-components": "0.5.0-dev.1"
|
|
13
14
|
},
|
|
14
15
|
"devDependencies": {
|
|
15
16
|
"tsdown": "0.18.4",
|
package/src/attestation.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canonicalizeJson } from './canonicalize-json';
|
|
3
|
-
import { readMigrationPackage
|
|
4
|
-
import type { MigrationManifest, MigrationOps } from './types';
|
|
3
|
+
import { readMigrationPackage } from './io';
|
|
4
|
+
import type { MigrationBundle, MigrationManifest, MigrationOps } from './types';
|
|
5
5
|
|
|
6
6
|
export interface VerifyResult {
|
|
7
7
|
readonly ok: boolean;
|
|
8
|
-
readonly reason?: '
|
|
8
|
+
readonly reason?: 'mismatch';
|
|
9
9
|
readonly storedMigrationId?: string;
|
|
10
10
|
readonly computedMigrationId?: string;
|
|
11
11
|
}
|
|
@@ -20,8 +20,15 @@ function sha256Hex(input: string): string {
|
|
|
20
20
|
* for the rationale: contracts are anchored separately by the
|
|
21
21
|
* storage-hash bookends inside the envelope; planner hints are advisory
|
|
22
22
|
* and must not affect identity.
|
|
23
|
+
*
|
|
24
|
+
* The `migrationId` field on the manifest is stripped before hashing so
|
|
25
|
+
* the function can be used both at write time (when no id exists yet)
|
|
26
|
+
* and at verify time (rehashing an already-attested manifest).
|
|
23
27
|
*/
|
|
24
|
-
export function computeMigrationId(
|
|
28
|
+
export function computeMigrationId(
|
|
29
|
+
manifest: Omit<MigrationManifest, 'migrationId'> & { readonly migrationId?: string },
|
|
30
|
+
ops: MigrationOps,
|
|
31
|
+
): string {
|
|
25
32
|
const {
|
|
26
33
|
migrationId: _migrationId,
|
|
27
34
|
signature: _signature,
|
|
@@ -40,34 +47,35 @@ export function computeMigrationId(manifest: MigrationManifest, ops: MigrationOp
|
|
|
40
47
|
return `sha256:${hash}`;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
export async function verifyMigration(dir: string): Promise<VerifyResult> {
|
|
55
|
-
const pkg = await readMigrationPackage(dir);
|
|
56
|
-
|
|
57
|
-
if (pkg.manifest.migrationId === null) {
|
|
58
|
-
return { ok: false, reason: 'draft' };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const computed = computeMigrationId(pkg.manifest, pkg.ops);
|
|
50
|
+
/**
|
|
51
|
+
* Re-hash an on-disk migration bundle and compare against the stored
|
|
52
|
+
* `migrationId`. Returns `{ ok: true }` when the package is internally
|
|
53
|
+
* consistent (manifest + ops still produce the recorded id), or
|
|
54
|
+
* `{ ok: false, reason: 'mismatch', stored, computed }` when they do
|
|
55
|
+
* not — typically a sign of FS corruption, partial writes, or a
|
|
56
|
+
* post-emit hand edit.
|
|
57
|
+
*/
|
|
58
|
+
export function verifyMigrationBundle(bundle: MigrationBundle): VerifyResult {
|
|
59
|
+
const computed = computeMigrationId(bundle.manifest, bundle.ops);
|
|
62
60
|
|
|
63
|
-
if (
|
|
64
|
-
return {
|
|
61
|
+
if (bundle.manifest.migrationId === computed) {
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
storedMigrationId: bundle.manifest.migrationId,
|
|
65
|
+
computedMigrationId: computed,
|
|
66
|
+
};
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
return {
|
|
68
70
|
ok: false,
|
|
69
71
|
reason: 'mismatch',
|
|
70
|
-
storedMigrationId:
|
|
72
|
+
storedMigrationId: bundle.manifest.migrationId,
|
|
71
73
|
computedMigrationId: computed,
|
|
72
74
|
};
|
|
73
75
|
}
|
|
76
|
+
|
|
77
|
+
/** Convenience wrapper: read the package from disk then verify it. */
|
|
78
|
+
export async function verifyMigration(dir: string): Promise<VerifyResult> {
|
|
79
|
+
const pkg = await readMigrationPackage(dir);
|
|
80
|
+
return verifyMigrationBundle(pkg);
|
|
81
|
+
}
|