@prisma-next/migration-tools 0.3.0-dev.99 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +10 -8
  2. package/dist/constants-9f-bCq8Y.mjs +10 -0
  3. package/dist/constants-9f-bCq8Y.mjs.map +1 -0
  4. package/dist/{errors-CqLiJwqA.mjs → errors-CSAAto11.mjs} +17 -17
  5. package/dist/errors-CSAAto11.mjs.map +1 -0
  6. package/dist/exports/attestation.d.mts +1 -1
  7. package/dist/exports/attestation.d.mts.map +1 -1
  8. package/dist/exports/attestation.mjs +4 -7
  9. package/dist/exports/attestation.mjs.map +1 -1
  10. package/dist/exports/constants.d.mts +9 -0
  11. package/dist/exports/constants.d.mts.map +1 -0
  12. package/dist/exports/constants.mjs +3 -0
  13. package/dist/exports/dag.d.mts +9 -9
  14. package/dist/exports/dag.mjs +18 -18
  15. package/dist/exports/dag.mjs.map +1 -1
  16. package/dist/exports/io.d.mts +6 -4
  17. package/dist/exports/io.d.mts.map +1 -1
  18. package/dist/exports/io.mjs +2 -2
  19. package/dist/exports/migration-ts.d.mts +34 -0
  20. package/dist/exports/migration-ts.d.mts.map +1 -0
  21. package/dist/exports/migration-ts.mjs +125 -0
  22. package/dist/exports/migration-ts.mjs.map +1 -0
  23. package/dist/exports/refs.mjs +1 -1
  24. package/dist/exports/types.d.mts +3 -3
  25. package/dist/exports/types.mjs +5 -2
  26. package/dist/exports/types.mjs.map +1 -1
  27. package/dist/{io-afog-e8J.mjs → io-LsuurzNb.mjs} +10 -4
  28. package/dist/io-LsuurzNb.mjs.map +1 -0
  29. package/dist/{types-9YQfIg6N.d.mts → types-BW_pJEe8.d.mts} +13 -9
  30. package/dist/types-BW_pJEe8.d.mts.map +1 -0
  31. package/package.json +12 -4
  32. package/src/attestation.ts +3 -5
  33. package/src/constants.ts +5 -0
  34. package/src/dag.ts +21 -21
  35. package/src/errors.ts +35 -27
  36. package/src/exports/constants.ts +1 -0
  37. package/src/exports/io.ts +2 -0
  38. package/src/exports/migration-ts.ts +6 -0
  39. package/src/exports/types.ts +5 -3
  40. package/src/io.ts +16 -5
  41. package/src/migration-ts.ts +199 -0
  42. package/src/types.ts +15 -7
  43. package/dist/errors-CqLiJwqA.mjs.map +0 -1
  44. package/dist/io-afog-e8J.mjs.map +0 -1
  45. package/dist/types-9YQfIg6N.d.mts.map +0 -1
@@ -0,0 +1,125 @@
1
+ import { stat, writeFile } from "node:fs/promises";
2
+ import { join, relative, resolve } from "pathe";
3
+
4
+ //#region src/migration-ts.ts
5
+ /**
6
+ * Utilities for scaffolding and evaluating migration.ts files.
7
+ *
8
+ * - scaffoldMigrationTs: writes a migration.ts file with boilerplate
9
+ * - evaluateMigrationTs: loads migration.ts via native Node import, returns descriptors
10
+ *
11
+ * Shared by migration plan (scaffold), migration new (scaffold), and
12
+ * migration verify (evaluate).
13
+ */
14
+ const MIGRATION_TS_FILE = "migration.ts";
15
+ function serializeQueryInput(input) {
16
+ if (typeof input === "boolean") return String(input);
17
+ if (typeof input === "symbol") return "TODO /* fill in using db.sql.from(...) */";
18
+ if (input === null || input === void 0) return "null";
19
+ if (Array.isArray(input)) {
20
+ if (input.length === 0) return "[]";
21
+ if (input.every((item) => typeof item === "symbol")) return "[TODO /* fill in using db.sql.from(...) */]";
22
+ return `[${input.map(serializeQueryInput).join(", ")}]`;
23
+ }
24
+ return JSON.stringify(input);
25
+ }
26
+ function descriptorToBuilderCall(desc) {
27
+ switch (desc.kind) {
28
+ case "createTable": return `createTable(${JSON.stringify(desc["table"])})`;
29
+ case "dropTable": return `dropTable(${JSON.stringify(desc["table"])})`;
30
+ case "addColumn": {
31
+ const args = [JSON.stringify(desc["table"]), JSON.stringify(desc["column"])];
32
+ if (desc["overrides"]) args.push(JSON.stringify(desc["overrides"]));
33
+ return `addColumn(${args.join(", ")})`;
34
+ }
35
+ case "dropColumn": return `dropColumn(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["column"])})`;
36
+ case "alterColumnType": {
37
+ const opts = {};
38
+ if (desc["using"]) opts["using"] = desc["using"];
39
+ if (desc["toType"]) opts["toType"] = desc["toType"];
40
+ return Object.keys(opts).length > 0 ? `alterColumnType(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["column"])}, ${JSON.stringify(opts)})` : `alterColumnType(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["column"])})`;
41
+ }
42
+ case "setNotNull": return `setNotNull(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["column"])})`;
43
+ case "dropNotNull": return `dropNotNull(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["column"])})`;
44
+ case "setDefault": return `setDefault(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["column"])})`;
45
+ case "dropDefault": return `dropDefault(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["column"])})`;
46
+ case "addPrimaryKey": return `addPrimaryKey(${JSON.stringify(desc["table"])})`;
47
+ case "addUnique": return `addUnique(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["columns"])})`;
48
+ case "addForeignKey": return `addForeignKey(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["columns"])})`;
49
+ case "dropConstraint": return `dropConstraint(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["constraintName"])})`;
50
+ case "createIndex": return `createIndex(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["columns"])})`;
51
+ case "dropIndex": return `dropIndex(${JSON.stringify(desc["table"])}, ${JSON.stringify(desc["indexName"])})`;
52
+ case "createEnumType": return desc["values"] ? `createEnumType(${JSON.stringify(desc["typeName"])}, ${JSON.stringify(desc["values"])})` : `createEnumType(${JSON.stringify(desc["typeName"])})`;
53
+ case "addEnumValues": return `addEnumValues(${JSON.stringify(desc["typeName"])}, ${JSON.stringify(desc["values"])})`;
54
+ case "dropEnumType": return `dropEnumType(${JSON.stringify(desc["typeName"])})`;
55
+ case "renameType": return `renameType(${JSON.stringify(desc["fromName"])}, ${JSON.stringify(desc["toName"])})`;
56
+ case "createDependency": return `createDependency(${JSON.stringify(desc["dependencyId"])})`;
57
+ case "dataTransform": return `dataTransform(${JSON.stringify(desc["name"])}, {\n check: ${serializeQueryInput(desc["check"])},\n run: ${serializeQueryInput(desc["run"])},\n })`;
58
+ default: throw new Error(`Unknown descriptor kind: ${desc.kind}`);
59
+ }
60
+ }
61
+ /**
62
+ * Scaffolds a migration.ts file in the given package directory.
63
+ * Serializes operation descriptors as builder calls that the user can edit.
64
+ * On verify, this file is re-evaluated to produce the final ops.
65
+ */
66
+ async function scaffoldMigrationTs(packageDir, options = {}) {
67
+ const filePath = join(packageDir, MIGRATION_TS_FILE);
68
+ const descriptors = options.descriptors ?? [];
69
+ const hasDataTransform = descriptors.some((d) => d.kind === "dataTransform");
70
+ const lines = [];
71
+ if (hasDataTransform && options.contractJsonPath) {
72
+ const relativeContractDts = relative(packageDir, options.contractJsonPath).replace(/\.json$/, ".d");
73
+ lines.push(`import type { Contract } from "${relativeContractDts}"`);
74
+ lines.push(`import { createBuilders } from "@prisma-next/target-postgres/migration-builders"`);
75
+ lines.push("");
76
+ const importList = [...new Set(descriptors.map((d) => d.kind))];
77
+ importList.push("TODO");
78
+ lines.push(`const { ${importList.join(", ")} } = createBuilders<Contract>()`);
79
+ } else {
80
+ const importList = [...new Set(descriptors.map((d) => d.kind))];
81
+ if (importList.length === 0) importList.push("createTable");
82
+ if (hasDataTransform) importList.push("TODO");
83
+ lines.push(`import { ${importList.join(", ")} } from "@prisma-next/target-postgres/migration-builders"`);
84
+ }
85
+ const calls = descriptors.map((d) => ` ${descriptorToBuilderCall(d)},`).join("\n");
86
+ const body = calls.length > 0 ? `\n${calls}\n` : "";
87
+ lines.push("");
88
+ lines.push(`export default () => [${body}]`);
89
+ lines.push("");
90
+ await writeFile(filePath, lines.join("\n"));
91
+ }
92
+ /**
93
+ * Checks whether a migration.ts file exists in the package directory.
94
+ */
95
+ async function hasMigrationTs(packageDir) {
96
+ try {
97
+ return (await stat(join(packageDir, MIGRATION_TS_FILE))).isFile();
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+ /**
103
+ * Evaluates a migration.ts file by loading it via native Node import.
104
+ * Returns the result of calling the default export (expected to be a
105
+ * function returning an array of operation descriptors).
106
+ *
107
+ * Requires Node ≥24 for native TypeScript support.
108
+ */
109
+ async function evaluateMigrationTs(packageDir) {
110
+ const filePath = resolve(join(packageDir, MIGRATION_TS_FILE));
111
+ try {
112
+ await stat(filePath);
113
+ } catch {
114
+ throw new Error(`migration.ts not found at "${filePath}"`);
115
+ }
116
+ const mod = await import(filePath);
117
+ if (typeof mod.default !== "function") throw new Error(`migration.ts must export a default function returning an operation list. Got: ${typeof mod.default}`);
118
+ const result = mod.default();
119
+ if (!Array.isArray(result)) throw new Error(`migration.ts default export must return an array of operations. Got: ${typeof result}`);
120
+ return result;
121
+ }
122
+
123
+ //#endregion
124
+ export { evaluateMigrationTs, hasMigrationTs, scaffoldMigrationTs };
125
+ //# sourceMappingURL=migration-ts.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-ts.mjs","names":["opts: Record<string, unknown>","lines: string[]","result: unknown"],"sources":["../../src/migration-ts.ts"],"sourcesContent":["/**\n * Utilities for scaffolding and evaluating migration.ts files.\n *\n * - scaffoldMigrationTs: writes a migration.ts file with boilerplate\n * - evaluateMigrationTs: loads migration.ts via native Node import, returns descriptors\n *\n * Shared by migration plan (scaffold), migration new (scaffold), and\n * migration verify (evaluate).\n */\n\nimport { stat, writeFile } from 'node:fs/promises';\nimport type { OperationDescriptor } from '@prisma-next/framework-components/control';\nimport { join, relative, resolve } from 'pathe';\n\nconst MIGRATION_TS_FILE = 'migration.ts';\n\n/**\n * Options for scaffolding a migration.ts file.\n */\nexport interface ScaffoldOptions {\n /** Operation descriptors to serialize as builder calls. */\n readonly descriptors?: readonly OperationDescriptor[];\n /** Absolute path to contract.json — used to derive contract.d.ts import for typed builders. */\n readonly contractJsonPath?: string;\n}\n\nfunction serializeQueryInput(input: unknown): string {\n if (typeof input === 'boolean') return String(input);\n if (typeof input === 'symbol') return 'TODO /* fill in using db.sql.from(...) */';\n if (input === null || input === undefined) return 'null';\n if (Array.isArray(input)) {\n if (input.length === 0) return '[]';\n if (input.every((item) => typeof item === 'symbol'))\n return '[TODO /* fill in using db.sql.from(...) */]';\n return `[${input.map(serializeQueryInput).join(', ')}]`;\n }\n return JSON.stringify(input);\n}\n\nfunction descriptorToBuilderCall(desc: OperationDescriptor): string {\n switch (desc.kind) {\n case 'createTable':\n return `createTable(${JSON.stringify(desc['table'])})`;\n case 'dropTable':\n return `dropTable(${JSON.stringify(desc['table'])})`;\n case 'addColumn': {\n const args = [JSON.stringify(desc['table']), JSON.stringify(desc['column'])];\n if (desc['overrides']) {\n args.push(JSON.stringify(desc['overrides']));\n }\n return `addColumn(${args.join(', ')})`;\n }\n case 'dropColumn':\n return `dropColumn(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;\n case 'alterColumnType': {\n const opts: Record<string, unknown> = {};\n if (desc['using']) opts['using'] = desc['using'];\n if (desc['toType']) opts['toType'] = desc['toType'];\n const hasOpts = Object.keys(opts).length > 0;\n return hasOpts\n ? `alterColumnType(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])}, ${JSON.stringify(opts)})`\n : `alterColumnType(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;\n }\n case 'setNotNull':\n return `setNotNull(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;\n case 'dropNotNull':\n return `dropNotNull(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;\n case 'setDefault':\n return `setDefault(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;\n case 'dropDefault':\n return `dropDefault(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['column'])})`;\n case 'addPrimaryKey':\n return `addPrimaryKey(${JSON.stringify(desc['table'])})`;\n case 'addUnique':\n return `addUnique(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['columns'])})`;\n case 'addForeignKey':\n return `addForeignKey(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['columns'])})`;\n case 'dropConstraint':\n return `dropConstraint(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['constraintName'])})`;\n case 'createIndex':\n return `createIndex(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['columns'])})`;\n case 'dropIndex':\n return `dropIndex(${JSON.stringify(desc['table'])}, ${JSON.stringify(desc['indexName'])})`;\n case 'createEnumType':\n return desc['values']\n ? `createEnumType(${JSON.stringify(desc['typeName'])}, ${JSON.stringify(desc['values'])})`\n : `createEnumType(${JSON.stringify(desc['typeName'])})`;\n case 'addEnumValues':\n return `addEnumValues(${JSON.stringify(desc['typeName'])}, ${JSON.stringify(desc['values'])})`;\n case 'dropEnumType':\n return `dropEnumType(${JSON.stringify(desc['typeName'])})`;\n case 'renameType':\n return `renameType(${JSON.stringify(desc['fromName'])}, ${JSON.stringify(desc['toName'])})`;\n case 'createDependency':\n return `createDependency(${JSON.stringify(desc['dependencyId'])})`;\n case 'dataTransform':\n return `dataTransform(${JSON.stringify(desc['name'])}, {\\n check: ${serializeQueryInput(desc['check'])},\\n run: ${serializeQueryInput(desc['run'])},\\n })`;\n default:\n throw new Error(`Unknown descriptor kind: ${desc.kind}`);\n }\n}\n\n/**\n * Scaffolds a migration.ts file in the given package directory.\n * Serializes operation descriptors as builder calls that the user can edit.\n * On verify, this file is re-evaluated to produce the final ops.\n */\nexport async function scaffoldMigrationTs(\n packageDir: string,\n options: ScaffoldOptions = {},\n): Promise<void> {\n const filePath = join(packageDir, MIGRATION_TS_FILE);\n\n const descriptors = options.descriptors ?? [];\n const hasDataTransform = descriptors.some((d) => d.kind === 'dataTransform');\n\n const lines: string[] = [];\n\n if (hasDataTransform && options.contractJsonPath) {\n const relativeContractDts = relative(packageDir, options.contractJsonPath).replace(\n /\\.json$/,\n '.d',\n );\n lines.push(`import type { Contract } from \"${relativeContractDts}\"`);\n lines.push(`import { createBuilders } from \"@prisma-next/target-postgres/migration-builders\"`);\n lines.push('');\n const importList = [...new Set(descriptors.map((d) => d.kind))];\n importList.push('TODO');\n lines.push(`const { ${importList.join(', ')} } = createBuilders<Contract>()`);\n } else {\n const importList = [...new Set(descriptors.map((d) => d.kind))];\n if (importList.length === 0) {\n importList.push('createTable');\n }\n if (hasDataTransform) {\n importList.push('TODO');\n }\n lines.push(\n `import { ${importList.join(', ')} } from \"@prisma-next/target-postgres/migration-builders\"`,\n );\n }\n\n const calls = descriptors.map((d) => ` ${descriptorToBuilderCall(d)},`).join('\\n');\n const body = calls.length > 0 ? `\\n${calls}\\n` : '';\n\n lines.push('');\n lines.push(`export default () => [${body}]`);\n lines.push('');\n\n await writeFile(filePath, lines.join('\\n'));\n}\n\n/**\n * Checks whether a migration.ts file exists in the package directory.\n */\nexport async function hasMigrationTs(packageDir: string): Promise<boolean> {\n try {\n const s = await stat(join(packageDir, MIGRATION_TS_FILE));\n return s.isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Evaluates a migration.ts file by loading it via native Node import.\n * Returns the result of calling the default export (expected to be a\n * function returning an array of operation descriptors).\n *\n * Requires Node ≥24 for native TypeScript support.\n */\nexport async function evaluateMigrationTs(packageDir: string): Promise<readonly unknown[]> {\n const filePath = resolve(join(packageDir, MIGRATION_TS_FILE));\n\n try {\n await stat(filePath);\n } catch {\n throw new Error(`migration.ts not found at \"${filePath}\"`);\n }\n\n // Use native Node TS import (Node ≥24, stable type stripping)\n const mod = (await import(filePath)) as { default?: unknown };\n\n if (typeof mod.default !== 'function') {\n throw new Error(\n `migration.ts must export a default function returning an operation list. Got: ${typeof mod.default}`,\n );\n }\n\n const result: unknown = mod.default();\n\n if (!Array.isArray(result)) {\n throw new Error(\n `migration.ts default export must return an array of operations. Got: ${typeof result}`,\n );\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,MAAM,oBAAoB;AAY1B,SAAS,oBAAoB,OAAwB;AACnD,KAAI,OAAO,UAAU,UAAW,QAAO,OAAO,MAAM;AACpD,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,KAAI,MAAM,QAAQ,MAAM,EAAE;AACxB,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,MAAI,MAAM,OAAO,SAAS,OAAO,SAAS,SAAS,CACjD,QAAO;AACT,SAAO,IAAI,MAAM,IAAI,oBAAoB,CAAC,KAAK,KAAK,CAAC;;AAEvD,QAAO,KAAK,UAAU,MAAM;;AAG9B,SAAS,wBAAwB,MAAmC;AAClE,SAAQ,KAAK,MAAb;EACE,KAAK,cACH,QAAO,eAAe,KAAK,UAAU,KAAK,SAAS,CAAC;EACtD,KAAK,YACH,QAAO,aAAa,KAAK,UAAU,KAAK,SAAS,CAAC;EACpD,KAAK,aAAa;GAChB,MAAM,OAAO,CAAC,KAAK,UAAU,KAAK,SAAS,EAAE,KAAK,UAAU,KAAK,UAAU,CAAC;AAC5E,OAAI,KAAK,aACP,MAAK,KAAK,KAAK,UAAU,KAAK,aAAa,CAAC;AAE9C,UAAO,aAAa,KAAK,KAAK,KAAK,CAAC;;EAEtC,KAAK,aACH,QAAO,cAAc,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,UAAU,CAAC;EACxF,KAAK,mBAAmB;GACtB,MAAMA,OAAgC,EAAE;AACxC,OAAI,KAAK,SAAU,MAAK,WAAW,KAAK;AACxC,OAAI,KAAK,UAAW,MAAK,YAAY,KAAK;AAE1C,UADgB,OAAO,KAAK,KAAK,CAAC,SAAS,IAEvC,mBAAmB,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,UAAU,CAAC,IAAI,KAAK,UAAU,KAAK,CAAC,KAC7G,mBAAmB,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,UAAU,CAAC;;EAE1F,KAAK,aACH,QAAO,cAAc,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,UAAU,CAAC;EACxF,KAAK,cACH,QAAO,eAAe,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,UAAU,CAAC;EACzF,KAAK,aACH,QAAO,cAAc,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,UAAU,CAAC;EACxF,KAAK,cACH,QAAO,eAAe,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,UAAU,CAAC;EACzF,KAAK,gBACH,QAAO,iBAAiB,KAAK,UAAU,KAAK,SAAS,CAAC;EACxD,KAAK,YACH,QAAO,aAAa,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,WAAW,CAAC;EACxF,KAAK,gBACH,QAAO,iBAAiB,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,WAAW,CAAC;EAC5F,KAAK,iBACH,QAAO,kBAAkB,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,kBAAkB,CAAC;EACpG,KAAK,cACH,QAAO,eAAe,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,WAAW,CAAC;EAC1F,KAAK,YACH,QAAO,aAAa,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI,KAAK,UAAU,KAAK,aAAa,CAAC;EAC1F,KAAK,iBACH,QAAO,KAAK,YACR,kBAAkB,KAAK,UAAU,KAAK,YAAY,CAAC,IAAI,KAAK,UAAU,KAAK,UAAU,CAAC,KACtF,kBAAkB,KAAK,UAAU,KAAK,YAAY,CAAC;EACzD,KAAK,gBACH,QAAO,iBAAiB,KAAK,UAAU,KAAK,YAAY,CAAC,IAAI,KAAK,UAAU,KAAK,UAAU,CAAC;EAC9F,KAAK,eACH,QAAO,gBAAgB,KAAK,UAAU,KAAK,YAAY,CAAC;EAC1D,KAAK,aACH,QAAO,cAAc,KAAK,UAAU,KAAK,YAAY,CAAC,IAAI,KAAK,UAAU,KAAK,UAAU,CAAC;EAC3F,KAAK,mBACH,QAAO,oBAAoB,KAAK,UAAU,KAAK,gBAAgB,CAAC;EAClE,KAAK,gBACH,QAAO,iBAAiB,KAAK,UAAU,KAAK,QAAQ,CAAC,kBAAkB,oBAAoB,KAAK,SAAS,CAAC,cAAc,oBAAoB,KAAK,OAAO,CAAC;EAC3J,QACE,OAAM,IAAI,MAAM,4BAA4B,KAAK,OAAO;;;;;;;;AAS9D,eAAsB,oBACpB,YACA,UAA2B,EAAE,EACd;CACf,MAAM,WAAW,KAAK,YAAY,kBAAkB;CAEpD,MAAM,cAAc,QAAQ,eAAe,EAAE;CAC7C,MAAM,mBAAmB,YAAY,MAAM,MAAM,EAAE,SAAS,gBAAgB;CAE5E,MAAMC,QAAkB,EAAE;AAE1B,KAAI,oBAAoB,QAAQ,kBAAkB;EAChD,MAAM,sBAAsB,SAAS,YAAY,QAAQ,iBAAiB,CAAC,QACzE,WACA,KACD;AACD,QAAM,KAAK,kCAAkC,oBAAoB,GAAG;AACpE,QAAM,KAAK,mFAAmF;AAC9F,QAAM,KAAK,GAAG;EACd,MAAM,aAAa,CAAC,GAAG,IAAI,IAAI,YAAY,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC;AAC/D,aAAW,KAAK,OAAO;AACvB,QAAM,KAAK,WAAW,WAAW,KAAK,KAAK,CAAC,iCAAiC;QACxE;EACL,MAAM,aAAa,CAAC,GAAG,IAAI,IAAI,YAAY,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC;AAC/D,MAAI,WAAW,WAAW,EACxB,YAAW,KAAK,cAAc;AAEhC,MAAI,iBACF,YAAW,KAAK,OAAO;AAEzB,QAAM,KACJ,YAAY,WAAW,KAAK,KAAK,CAAC,2DACnC;;CAGH,MAAM,QAAQ,YAAY,KAAK,MAAM,KAAK,wBAAwB,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK;CACnF,MAAM,OAAO,MAAM,SAAS,IAAI,KAAK,MAAM,MAAM;AAEjD,OAAM,KAAK,GAAG;AACd,OAAM,KAAK,yBAAyB,KAAK,GAAG;AAC5C,OAAM,KAAK,GAAG;AAEd,OAAM,UAAU,UAAU,MAAM,KAAK,KAAK,CAAC;;;;;AAM7C,eAAsB,eAAe,YAAsC;AACzE,KAAI;AAEF,UADU,MAAM,KAAK,KAAK,YAAY,kBAAkB,CAAC,EAChD,QAAQ;SACX;AACN,SAAO;;;;;;;;;;AAWX,eAAsB,oBAAoB,YAAiD;CACzF,MAAM,WAAW,QAAQ,KAAK,YAAY,kBAAkB,CAAC;AAE7D,KAAI;AACF,QAAM,KAAK,SAAS;SACd;AACN,QAAM,IAAI,MAAM,8BAA8B,SAAS,GAAG;;CAI5D,MAAM,MAAO,MAAM,OAAO;AAE1B,KAAI,OAAO,IAAI,YAAY,WACzB,OAAM,IAAI,MACR,iFAAiF,OAAO,IAAI,UAC7F;CAGH,MAAMC,SAAkB,IAAI,SAAS;AAErC,KAAI,CAAC,MAAM,QAAQ,OAAO,CACxB,OAAM,IAAI,MACR,wEAAwE,OAAO,SAChF;AAGH,QAAO"}
@@ -1,4 +1,4 @@
1
- import { c as errorInvalidRefValue, l as errorInvalidRefs, s as errorInvalidRefName, t as MigrationToolsError } from "../errors-CqLiJwqA.mjs";
1
+ import { c as errorInvalidRefValue, l as errorInvalidRefs, s as errorInvalidRefName, t as MigrationToolsError } from "../errors-CSAAto11.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";
@@ -1,4 +1,4 @@
1
- import { a as MigrationChainEntry, c as MigrationManifest, i as MigrationBundle, l as MigrationOps, n as AttestedMigrationManifest, o as MigrationGraph, r as DraftMigrationManifest, s as MigrationHints, t as AttestedMigrationBundle, u as isAttested } from "../types-9YQfIg6N.mjs";
1
+ import { a as DraftMigrationManifest, c as MigrationHints, d as isAttested, f as isDraft, i as DraftMigrationBundle, l as MigrationManifest, n as AttestedMigrationManifest, o as MigrationChainEntry, r as BaseMigrationBundle, s as MigrationGraph, t as AttestedMigrationBundle, u as MigrationOps } from "../types-BW_pJEe8.mjs";
2
2
 
3
3
  //#region src/errors.d.ts
4
4
 
@@ -7,7 +7,7 @@ import { a as MigrationChainEntry, c as MigrationManifest, i as MigrationBundle,
7
7
  *
8
8
  * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under
9
9
  * the MIGRATION namespace. These are tooling-time errors (file I/O, attestation,
10
- * migration-chain reconstruction), distinct from the runtime MIGRATION.* codes for apply-time
10
+ * migration history reconstruction), distinct from the runtime MIGRATION.* codes for apply-time
11
11
  * failures (PRECHECK_FAILED, POSTCHECK_FAILED, etc.).
12
12
  *
13
13
  * Fields:
@@ -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 AttestedMigrationBundle, type AttestedMigrationManifest, type DraftMigrationManifest, type MigrationBundle, type MigrationBundle as MigrationPackage, type MigrationChainEntry, type MigrationGraph, type MigrationHints, type MigrationManifest, type MigrationOps, MigrationToolsError, isAttested };
34
+ export { type AttestedMigrationBundle, type AttestedMigrationManifest, type BaseMigrationBundle, type BaseMigrationBundle as MigrationBundle, type BaseMigrationBundle as MigrationPackage, type DraftMigrationBundle, type DraftMigrationManifest, type MigrationChainEntry, type MigrationGraph, type MigrationHints, type MigrationManifest, type MigrationOps, MigrationToolsError, isAttested, isDraft };
35
35
  //# sourceMappingURL=types.d.mts.map
@@ -1,4 +1,4 @@
1
- import { t as MigrationToolsError } from "../errors-CqLiJwqA.mjs";
1
+ import { t as MigrationToolsError } from "../errors-CSAAto11.mjs";
2
2
 
3
3
  //#region src/types.ts
4
4
  /**
@@ -8,7 +8,10 @@ import { t as MigrationToolsError } from "../errors-CqLiJwqA.mjs";
8
8
  function isAttested(bundle) {
9
9
  return typeof bundle.manifest.migrationId === "string";
10
10
  }
11
+ function isDraft(bundle) {
12
+ return bundle.manifest.migrationId === null;
13
+ }
11
14
 
12
15
  //#endregion
13
- export { MigrationToolsError, isAttested };
16
+ export { MigrationToolsError, isAttested, isDraft };
14
17
  //# sourceMappingURL=types.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.mjs","names":[],"sources":["../../src/types.ts"],"sourcesContent":["import type { ContractIR } from '@prisma-next/contract/ir';\nimport type { MigrationPlanOperation } from '@prisma-next/core-control-plane/types';\n\nexport interface MigrationHints {\n readonly used: readonly string[];\n readonly applied: readonly string[];\n readonly plannerVersion: string;\n readonly planningStrategy: string;\n}\n\n/**\n * Shared fields for all migration manifests (draft and attested).\n */\ninterface MigrationManifestBase {\n readonly from: string;\n readonly to: string;\n readonly kind: 'regular' | 'baseline';\n readonly fromContract: ContractIR | null;\n readonly toContract: ContractIR;\n readonly hints: MigrationHints;\n readonly labels: readonly string[];\n readonly authorship?: { readonly author?: string; readonly email?: string };\n readonly signature?: { readonly keyId: string; readonly value: string } | null;\n readonly createdAt: string;\n}\n\n/**\n * A draft migration that has been planned but not yet attested.\n * Draft migrations have `migrationId: null` and are excluded from\n * graph reconstruction and apply.\n */\nexport interface DraftMigrationManifest extends MigrationManifestBase {\n readonly migrationId: null;\n}\n\n/**\n * An attested migration with a content-addressed migrationId.\n * Only attested migrations participate in the migration graph.\n */\nexport interface AttestedMigrationManifest extends MigrationManifestBase {\n readonly migrationId: string;\n}\n\n/**\n * Union of draft and attested manifests. This is what the on-disk\n * format represents — `migrationId` is `null` for drafts, a string\n * for attested migrations.\n */\nexport type MigrationManifest = DraftMigrationManifest | AttestedMigrationManifest;\n\nexport type MigrationOps = readonly MigrationPlanOperation[];\n\n/**\n * An on-disk migration directory containing a manifest and operations.\n * The manifest may be draft or attested.\n */\nexport interface MigrationBundle {\n readonly dirName: string;\n readonly dirPath: string;\n readonly manifest: MigrationManifest;\n readonly ops: MigrationOps;\n}\n\n/**\n * A bundle known to be attested (migrationId is a string).\n * Use this after filtering bundles to attested-only.\n */\nexport interface AttestedMigrationBundle extends MigrationBundle {\n readonly manifest: AttestedMigrationManifest;\n}\n\n/**\n * An entry in the migration graph. Only attested migrations appear in the\n * graph, so `migrationId` is always a string.\n */\nexport interface MigrationChainEntry {\n readonly from: string;\n readonly to: string;\n readonly migrationId: string;\n readonly dirName: string;\n readonly createdAt: string;\n readonly labels: readonly string[];\n}\n\nexport interface MigrationGraph {\n readonly nodes: ReadonlySet<string>;\n readonly forwardChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;\n readonly reverseChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;\n readonly migrationById: ReadonlyMap<string, MigrationChainEntry>;\n}\n\n/**\n * Type guard that narrows a MigrationBundle to an AttestedMigrationBundle.\n * Use with `.filter(isAttested)` to get a typed array of attested bundles.\n */\nexport function isAttested(bundle: MigrationBundle): bundle is AttestedMigrationBundle {\n return typeof bundle.manifest.migrationId === 'string';\n}\n"],"mappings":";;;;;;;AA+FA,SAAgB,WAAW,QAA4D;AACrF,QAAO,OAAO,OAAO,SAAS,gBAAgB"}
1
+ {"version":3,"file":"types.mjs","names":[],"sources":["../../src/types.ts"],"sourcesContent":["import type { Contract } from '@prisma-next/contract/types';\nimport type { MigrationPlanOperation } from '@prisma-next/framework-components/control';\n\nexport interface MigrationHints {\n readonly used: readonly string[];\n readonly applied: readonly string[];\n readonly plannerVersion: string;\n readonly planningStrategy: string;\n}\n\n/**\n * Shared fields for all migration manifests (draft and attested).\n */\ninterface MigrationManifestBase {\n readonly from: string;\n readonly to: string;\n readonly kind: 'regular' | 'baseline';\n readonly fromContract: Contract | null;\n readonly toContract: Contract;\n readonly hints: MigrationHints;\n readonly labels: readonly string[];\n readonly authorship?: { readonly author?: string; readonly email?: string };\n readonly signature?: { readonly keyId: string; readonly value: string } | null;\n readonly createdAt: string;\n}\n\n/**\n * A draft migration that has been planned but not yet attested.\n * Draft migrations have `migrationId: null` and are excluded from\n * graph reconstruction and apply.\n */\nexport interface DraftMigrationManifest extends MigrationManifestBase {\n readonly migrationId: null;\n}\n\n/**\n * An attested migration with a content-addressed migrationId.\n * Only attested migrations participate in the migration graph.\n */\nexport interface AttestedMigrationManifest extends MigrationManifestBase {\n readonly migrationId: string;\n}\n\n/**\n * Union of draft and attested manifests. This is what the on-disk\n * format represents — `migrationId` is `null` for drafts, a string\n * for attested migrations.\n */\nexport type MigrationManifest = DraftMigrationManifest | AttestedMigrationManifest;\n\nexport type MigrationOps = readonly MigrationPlanOperation[];\n\n/**\n * An on-disk migration directory containing a manifest and operations.\n * The manifest may be draft or attested.\n */\nexport interface BaseMigrationBundle {\n readonly dirName: string;\n readonly dirPath: string;\n readonly manifest: MigrationManifest;\n readonly ops: MigrationOps;\n}\n\n/**\n * A bundle known to be attested (migrationId is a string).\n * Use this after filtering bundles to attested-only.\n */\nexport interface AttestedMigrationBundle extends BaseMigrationBundle {\n readonly manifest: AttestedMigrationManifest;\n}\n\nexport interface DraftMigrationBundle extends BaseMigrationBundle {\n readonly manifest: DraftMigrationManifest;\n}\n\n/**\n * An entry in the migration graph. Only attested migrations appear in the\n * graph, so `migrationId` is always a string.\n */\nexport interface MigrationChainEntry {\n readonly from: string;\n readonly to: string;\n readonly migrationId: string;\n readonly dirName: string;\n readonly createdAt: string;\n readonly labels: readonly string[];\n}\n\nexport interface MigrationGraph {\n readonly nodes: ReadonlySet<string>;\n readonly forwardChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;\n readonly reverseChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;\n readonly migrationById: ReadonlyMap<string, MigrationChainEntry>;\n}\n\n/**\n * Type guard that narrows a MigrationBundle to an AttestedMigrationBundle.\n * Use with `.filter(isAttested)` to get a typed array of attested bundles.\n */\nexport function isAttested(bundle: BaseMigrationBundle): bundle is AttestedMigrationBundle {\n return typeof bundle.manifest.migrationId === 'string';\n}\n\nexport function isDraft(bundle: BaseMigrationBundle): bundle is DraftMigrationBundle {\n return bundle.manifest.migrationId === null;\n}\n"],"mappings":";;;;;;;AAmGA,SAAgB,WAAW,QAAgE;AACzF,QAAO,OAAO,OAAO,SAAS,gBAAgB;;AAGhD,SAAgB,QAAQ,QAA6D;AACnF,QAAO,OAAO,SAAS,gBAAgB"}
@@ -1,4 +1,4 @@
1
- import { a as errorInvalidJson, d as errorMissingFile, o as errorInvalidManifest, r as errorDirectoryExists, u as errorInvalidSlug } from "./errors-CqLiJwqA.mjs";
1
+ import { a as errorInvalidJson, d as errorMissingFile, o as errorInvalidManifest, r as errorDirectoryExists, u as errorInvalidSlug } from "./errors-CSAAto11.mjs";
2
2
  import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
3
  import { type } from "arktype";
4
4
  import { basename, dirname, join } from "pathe";
@@ -37,7 +37,7 @@ const MigrationManifestSchema = type({
37
37
  const MigrationOpsSchema = type({
38
38
  id: "string",
39
39
  label: "string",
40
- operationClass: "'additive' | 'widening' | 'destructive'"
40
+ operationClass: "'additive' | 'widening' | 'destructive' | 'data'"
41
41
  }).array();
42
42
  async function writeMigrationPackage(dir, manifest, ops) {
43
43
  await mkdir(dirname(dir), { recursive: true });
@@ -50,6 +50,12 @@ async function writeMigrationPackage(dir, manifest, ops) {
50
50
  await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2), { flag: "wx" });
51
51
  await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: "wx" });
52
52
  }
53
+ async function writeMigrationManifest(dir, manifest) {
54
+ await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
55
+ }
56
+ async function writeMigrationOps(dir, ops) {
57
+ await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\n`);
58
+ }
53
59
  async function readMigrationPackage(dir) {
54
60
  const manifestPath = join(dir, MANIFEST_FILE);
55
61
  const opsPath = join(dir, OPS_FILE);
@@ -126,5 +132,5 @@ function formatMigrationDirName(timestamp, slug) {
126
132
  }
127
133
 
128
134
  //#endregion
129
- export { writeMigrationPackage as i, readMigrationPackage as n, readMigrationsDir as r, formatMigrationDirName as t };
130
- //# sourceMappingURL=io-afog-e8J.mjs.map
135
+ export { writeMigrationOps as a, writeMigrationManifest as i, readMigrationPackage as n, writeMigrationPackage as o, readMigrationsDir as r, formatMigrationDirName as t };
136
+ //# sourceMappingURL=io-LsuurzNb.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io-LsuurzNb.mjs","names":["manifestRaw: string","opsRaw: string","manifest: MigrationManifest","ops: MigrationOps","entries: string[]","packages: BaseMigrationBundle[]"],"sources":["../src/io.ts"],"sourcesContent":["import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport {\n errorDirectoryExists,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMissingFile,\n} from './errors';\nimport type { BaseMigrationBundle, 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 planningStrategy: 'string',\n});\n\nconst MigrationManifestSchema = type({\n from: 'string',\n to: 'string',\n migrationId: 'string | null',\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\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<BaseMigrationBundle> {\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 BaseMigrationBundle[]> {\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: BaseMigrationBundle[] = [];\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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"}
@@ -1,5 +1,5 @@
1
- import { ContractIR } from "@prisma-next/contract/ir";
2
- import { MigrationPlanOperation } from "@prisma-next/core-control-plane/types";
1
+ import { Contract } from "@prisma-next/contract/types";
2
+ import { MigrationPlanOperation } from "@prisma-next/framework-components/control";
3
3
 
4
4
  //#region src/types.d.ts
5
5
  interface MigrationHints {
@@ -15,8 +15,8 @@ interface MigrationManifestBase {
15
15
  readonly from: string;
16
16
  readonly to: string;
17
17
  readonly kind: 'regular' | 'baseline';
18
- readonly fromContract: ContractIR | null;
19
- readonly toContract: ContractIR;
18
+ readonly fromContract: Contract | null;
19
+ readonly toContract: Contract;
20
20
  readonly hints: MigrationHints;
21
21
  readonly labels: readonly string[];
22
22
  readonly authorship?: {
@@ -55,7 +55,7 @@ type MigrationOps = readonly MigrationPlanOperation[];
55
55
  * An on-disk migration directory containing a manifest and operations.
56
56
  * The manifest may be draft or attested.
57
57
  */
58
- interface MigrationBundle {
58
+ interface BaseMigrationBundle {
59
59
  readonly dirName: string;
60
60
  readonly dirPath: string;
61
61
  readonly manifest: MigrationManifest;
@@ -65,9 +65,12 @@ interface MigrationBundle {
65
65
  * A bundle known to be attested (migrationId is a string).
66
66
  * Use this after filtering bundles to attested-only.
67
67
  */
68
- interface AttestedMigrationBundle extends MigrationBundle {
68
+ interface AttestedMigrationBundle extends BaseMigrationBundle {
69
69
  readonly manifest: AttestedMigrationManifest;
70
70
  }
71
+ interface DraftMigrationBundle extends BaseMigrationBundle {
72
+ readonly manifest: DraftMigrationManifest;
73
+ }
71
74
  /**
72
75
  * An entry in the migration graph. Only attested migrations appear in the
73
76
  * graph, so `migrationId` is always a string.
@@ -90,7 +93,8 @@ interface MigrationGraph {
90
93
  * Type guard that narrows a MigrationBundle to an AttestedMigrationBundle.
91
94
  * Use with `.filter(isAttested)` to get a typed array of attested bundles.
92
95
  */
93
- declare function isAttested(bundle: MigrationBundle): bundle is AttestedMigrationBundle;
96
+ declare function isAttested(bundle: BaseMigrationBundle): bundle is AttestedMigrationBundle;
97
+ declare function isDraft(bundle: BaseMigrationBundle): bundle is DraftMigrationBundle;
94
98
  //#endregion
95
- export { MigrationChainEntry as a, MigrationManifest as c, MigrationBundle as i, MigrationOps as l, AttestedMigrationManifest as n, MigrationGraph as o, DraftMigrationManifest as r, MigrationHints as s, AttestedMigrationBundle as t, isAttested as u };
96
- //# sourceMappingURL=types-9YQfIg6N.d.mts.map
99
+ export { DraftMigrationManifest as a, MigrationHints as c, isAttested as d, isDraft as f, DraftMigrationBundle as i, MigrationManifest as l, AttestedMigrationManifest as n, MigrationChainEntry as o, BaseMigrationBundle as r, MigrationGraph as s, AttestedMigrationBundle as t, MigrationOps as u };
100
+ //# sourceMappingURL=types-BW_pJEe8.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-BW_pJEe8.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;UAGiB,cAAA;;EAAA,SAAA,OAAA,EAAc,SAAA,MAAA,EAAA;EAUrB,SAAA,cAAA,EAAqB,MAAA;EAIN,SAAA,gBAAA,EAAA,MAAA;;;;AAczB;AAQA,UA1BU,qBAAA,CA0BiC;EAS/B,SAAA,IAAA,EAAA,MAAiB;EAEjB,SAAA,EAAA,EAAA,MAAY;EAMP,SAAA,IAAA,EAAA,SAAmB,GAAA,UAGf;EAQJ,SAAA,YAAA,EAlDQ,QAkDgB,GAAA,IACpB;EAGJ,SAAA,UAAA,EArDM,QAqDe;EAQrB,SAAA,KAAA,EA5DC,cA4DkB;EASnB,SAAA,MAAA,EAAc,SAAA,MAAA,EAAA;EACb,SAAA,UAAA,CAAA,EAAA;IACoC,SAAA,MAAA,CAAA,EAAA,MAAA;IAA7B,SAAA,KAAA,CAAA,EAAA,MAAA;EAC6B,CAAA;EAA7B,SAAA,SAAA,CAAA,EAAA;IACqB,SAAA,KAAA,EAAA,MAAA;IAApB,SAAA,KAAA,EAAA,MAAA;EAAW,CAAA,GAAA,IAAA;EAOrB,SAAA,SAAU,EAAA,MAAS;AAInC;;;;;;UAxEiB,sBAAA,SAA+B;;;;;;;UAQ/B,yBAAA,SAAkC;;;;;;;;KASvC,iBAAA,GAAoB,yBAAyB;KAE7C,YAAA,YAAwB;;;;;UAMnB,mBAAA;;;qBAGI;gBACL;;;;;;UAOC,uBAAA,SAAgC;qBAC5B;;UAGJ,oBAAA,SAA6B;qBACzB;;;;;;UAOJ,mBAAA;;;;;;;;UASA,cAAA;kBACC;yBACO,6BAA6B;yBAC7B,6BAA6B;0BAC5B,oBAAoB;;;;;;iBAO9B,UAAA,SAAmB,gCAAgC;iBAInD,OAAA,SAAgB,gCAAgC"}
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@prisma-next/migration-tools",
3
- "version": "0.3.0-dev.99",
3
+ "version": "0.3.0",
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
- "@prisma-next/contract": "0.3.0-dev.99",
11
- "@prisma-next/utils": "0.3.0-dev.99",
12
- "@prisma-next/core-control-plane": "0.3.0-dev.99"
10
+ "@prisma-next/contract": "0.3.0",
11
+ "@prisma-next/framework-components": "0.3.0",
12
+ "@prisma-next/utils": "0.3.0"
13
13
  },
14
14
  "devDependencies": {
15
15
  "tsdown": "0.18.4",
@@ -46,6 +46,14 @@
46
46
  "types": "./dist/exports/refs.d.mts",
47
47
  "import": "./dist/exports/refs.mjs"
48
48
  },
49
+ "./constants": {
50
+ "types": "./dist/exports/constants.d.mts",
51
+ "import": "./dist/exports/constants.mjs"
52
+ },
53
+ "./migration-ts": {
54
+ "types": "./dist/exports/migration-ts.d.mts",
55
+ "import": "./dist/exports/migration-ts.mjs"
56
+ },
49
57
  "./package.json": "./package.json"
50
58
  },
51
59
  "repository": {
@@ -1,9 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { writeFile } from 'node:fs/promises';
3
- import { canonicalizeContract } from '@prisma-next/core-control-plane/emission';
4
- import { join } from 'pathe';
2
+ import { canonicalizeContract } from '@prisma-next/contract/hashing';
5
3
  import { canonicalizeJson } from './canonicalize-json';
6
- import { readMigrationPackage } from './io';
4
+ import { readMigrationPackage, writeMigrationManifest } from './io';
7
5
  import type { MigrationManifest, MigrationOps } from './types';
8
6
 
9
7
  export interface VerifyResult {
@@ -49,7 +47,7 @@ export async function attestMigration(dir: string): Promise<string> {
49
47
  const migrationId = computeMigrationId(pkg.manifest, pkg.ops);
50
48
 
51
49
  const updated = { ...pkg.manifest, migrationId };
52
- await writeFile(join(dir, 'migration.json'), JSON.stringify(updated, null, 2));
50
+ await writeMigrationManifest(dir, updated);
53
51
 
54
52
  return migrationId;
55
53
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Sentinel value representing the absence of a contract (empty/new project).
3
+ * This is a human-readable marker, not a real SHA-256 hash.
4
+ */
5
+ export const EMPTY_CONTRACT_HASH = 'sha256:empty' as const;
package/src/dag.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { EMPTY_CONTRACT_HASH } from '@prisma-next/core-control-plane/constants';
2
1
  import { ifDefined } from '@prisma-next/utils/defined';
2
+ import { EMPTY_CONTRACT_HASH } from './constants';
3
3
  import {
4
- errorAmbiguousLeaf,
4
+ errorAmbiguousTarget,
5
5
  errorDuplicateMigrationId,
6
- errorNoResolvableLeaf,
7
- errorNoRoot,
8
- errorSelfLoop,
6
+ errorNoInitialMigration,
7
+ errorNoTarget,
8
+ errorSameSourceAndTarget,
9
9
  } from './errors';
10
10
  import type { AttestedMigrationBundle, MigrationChainEntry, MigrationGraph } from './types';
11
11
 
@@ -19,7 +19,7 @@ export function reconstructGraph(packages: readonly AttestedMigrationBundle[]):
19
19
  const { from, to } = pkg.manifest;
20
20
 
21
21
  if (from === to) {
22
- throw errorSelfLoop(pkg.dirName, from);
22
+ throw errorSameSourceAndTarget(pkg.dirName, from);
23
23
  }
24
24
 
25
25
  nodes.add(from);
@@ -203,7 +203,7 @@ export function findPathWithDecision(
203
203
  }
204
204
 
205
205
  /**
206
- * Walk ancestors of each leaf back from the leaves to find the last node
206
+ * Walk ancestors of each branch tip back to find the last node
207
207
  * that appears on all paths. Returns `fromHash` if no shared ancestor is found.
208
208
  */
209
209
  function findDivergencePoint(
@@ -246,8 +246,8 @@ function findDivergencePoint(
246
246
  }
247
247
 
248
248
  /**
249
- * Find all leaf nodes reachable from `fromHash` via forward edges.
250
- * A leaf is a node with no outgoing edges in the graph.
249
+ * Find all branch tips (nodes with no outgoing edges) reachable from
250
+ * `fromHash` via forward edges.
251
251
  */
252
252
  export function findReachableLeaves(graph: MigrationGraph, fromHash: string): readonly string[] {
253
253
  const visited = new Set<string>();
@@ -276,10 +276,10 @@ export function findReachableLeaves(graph: MigrationGraph, fromHash: string): re
276
276
  }
277
277
 
278
278
  /**
279
- * Find the leaf contract hash of the migration graph reachable from
280
- * EMPTY_CONTRACT_HASH. Throws NO_ROOT if the graph has nodes but none
281
- * originate from the empty hash (e.g. root migration was deleted).
282
- * Throws AMBIGUOUS_LEAF if multiple leaves exist.
279
+ * Find the target contract hash of the migration graph reachable from
280
+ * EMPTY_CONTRACT_HASH. Throws NO_INITIAL_MIGRATION if the graph has
281
+ * nodes but none originate from the empty hash.
282
+ * Throws AMBIGUOUS_TARGET if multiple branch tips exist.
283
283
  */
284
284
  export function findLeaf(graph: MigrationGraph): string {
285
285
  if (graph.nodes.size === 0) {
@@ -287,7 +287,7 @@ export function findLeaf(graph: MigrationGraph): string {
287
287
  }
288
288
 
289
289
  if (!graph.nodes.has(EMPTY_CONTRACT_HASH)) {
290
- throw errorNoRoot([...graph.nodes]);
290
+ throw errorNoInitialMigration([...graph.nodes]);
291
291
  }
292
292
 
293
293
  const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);
@@ -295,21 +295,21 @@ export function findLeaf(graph: MigrationGraph): string {
295
295
  if (leaves.length === 0) {
296
296
  const reachable = [...graph.nodes].filter((n) => n !== EMPTY_CONTRACT_HASH);
297
297
  if (reachable.length > 0) {
298
- throw errorNoResolvableLeaf(reachable);
298
+ throw errorNoTarget(reachable);
299
299
  }
300
300
  return EMPTY_CONTRACT_HASH;
301
301
  }
302
302
 
303
303
  if (leaves.length > 1) {
304
304
  const divergencePoint = findDivergencePoint(graph, EMPTY_CONTRACT_HASH, leaves);
305
- const branches = leaves.map((leaf) => {
306
- const path = findPath(graph, divergencePoint, leaf);
305
+ const branches = leaves.map((tip) => {
306
+ const path = findPath(graph, divergencePoint, tip);
307
307
  return {
308
- leaf,
308
+ tip,
309
309
  edges: (path ?? []).map((e) => ({ dirName: e.dirName, from: e.from, to: e.to })),
310
310
  };
311
311
  });
312
- throw errorAmbiguousLeaf(leaves, { divergencePoint, branches });
312
+ throw errorAmbiguousTarget(leaves, { divergencePoint, branches });
313
313
  }
314
314
 
315
315
  const leaf = leaves[0];
@@ -318,8 +318,8 @@ export function findLeaf(graph: MigrationGraph): string {
318
318
 
319
319
  /**
320
320
  * Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH
321
- * to the single leaf. Returns null for an empty graph.
322
- * Throws AMBIGUOUS_LEAF if the graph has multiple leaves.
321
+ * to the single target. Returns null for an empty graph.
322
+ * Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.
323
323
  */
324
324
  export function findLatestMigration(graph: MigrationGraph): MigrationChainEntry | null {
325
325
  if (graph.nodes.size === 0) {
package/src/errors.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under
5
5
  * the MIGRATION namespace. These are tooling-time errors (file I/O, attestation,
6
- * migration-chain reconstruction), distinct from the runtime MIGRATION.* codes for apply-time
6
+ * migration history reconstruction), distinct from the runtime MIGRATION.* codes for apply-time
7
7
  * failures (PRECHECK_FAILED, POSTCHECK_FAILED, etc.).
8
8
  *
9
9
  * Fields:
@@ -84,40 +84,44 @@ export function errorInvalidSlug(slug: string): MigrationToolsError {
84
84
  });
85
85
  }
86
86
 
87
- export function errorSelfLoop(dirName: string, hash: string): MigrationToolsError {
88
- return new MigrationToolsError('MIGRATION.SELF_LOOP', 'Self-loop in migration graph', {
89
- why: `Migration "${dirName}" has from === to === "${hash}". A migration must transition between two different contract states.`,
90
- fix: 'Delete the invalid migration directory and re-run migration plan.',
91
- details: { dirName, hash },
92
- });
87
+ export function errorSameSourceAndTarget(dirName: string, hash: string): MigrationToolsError {
88
+ return new MigrationToolsError(
89
+ 'MIGRATION.SAME_SOURCE_AND_TARGET',
90
+ 'Migration has same source and target',
91
+ {
92
+ why: `Migration "${dirName}" has from === to === "${hash}". A migration must transition between two different contract states.`,
93
+ fix: 'Delete the invalid migration directory and re-run migration plan.',
94
+ details: { dirName, hash },
95
+ },
96
+ );
93
97
  }
94
98
 
95
- export function errorAmbiguousLeaf(
96
- leaves: readonly string[],
99
+ export function errorAmbiguousTarget(
100
+ branchTips: readonly string[],
97
101
  context?: {
98
102
  divergencePoint: string;
99
103
  branches: readonly {
100
- leaf: string;
104
+ tip: string;
101
105
  edges: readonly { dirName: string; from: string; to: string }[];
102
106
  }[];
103
107
  },
104
108
  ): MigrationToolsError {
105
109
  const divergenceInfo = context
106
- ? `\nDivergence point: ${context.divergencePoint}\nBranches:\n${context.branches.map((b) => ` → ${b.leaf} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(' → ') || 'direct'})`).join('\n')}`
110
+ ? `\nDivergence point: ${context.divergencePoint}\nBranches:\n${context.branches.map((b) => ` → ${b.tip} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(' → ') || 'direct'})`).join('\n')}`
107
111
  : '';
108
- return new MigrationToolsError('MIGRATION.AMBIGUOUS_LEAF', 'Ambiguous migration graph', {
109
- why: `Multiple leaf nodes found: ${leaves.join(', ')}. The migration graph has diverged — this typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,
112
+ return new MigrationToolsError('MIGRATION.AMBIGUOUS_TARGET', 'Ambiguous migration target', {
113
+ why: `The migration history has diverged into multiple branches: ${branchTips.join(', ')}. This typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,
110
114
  fix: 'Use `migration ref set <name> <hash>` to target a specific branch, delete one of the conflicting migration directories and re-run `migration plan`, or use --from <hash> to explicitly select a starting point.',
111
115
  details: {
112
- leaves,
116
+ branchTips,
113
117
  ...(context ? { divergencePoint: context.divergencePoint, branches: context.branches } : {}),
114
118
  },
115
119
  });
116
120
  }
117
121
 
118
- export function errorNoRoot(nodes: readonly string[]): MigrationToolsError {
119
- return new MigrationToolsError('MIGRATION.NO_ROOT', 'Migration graph has no root', {
120
- why: `No root migration found in the migration graph (nodes: ${nodes.join(', ')}). No migration starts from the empty contract hash, or all edges form a disconnected subgraph.`,
122
+ export function errorNoInitialMigration(nodes: readonly string[]): MigrationToolsError {
123
+ return new MigrationToolsError('MIGRATION.NO_INITIAL_MIGRATION', 'No initial migration found', {
124
+ why: `No migration starts from the empty contract state (known hashes: ${nodes.join(', ')}). At least one migration must originate from the empty state.`,
121
125
  fix: 'Inspect the migrations directory for corrupted migration.json files. At least one migration must start from the empty contract hash.',
122
126
  details: { nodes },
123
127
  });
@@ -131,6 +135,14 @@ export function errorInvalidRefs(refsPath: string, reason: string): MigrationToo
131
135
  });
132
136
  }
133
137
 
138
+ export function errorInvalidRefFile(filePath: string, reason: string): MigrationToolsError {
139
+ return new MigrationToolsError('MIGRATION.INVALID_REF_FILE', 'Invalid ref file', {
140
+ why: `Ref file at "${filePath}" is invalid: ${reason}`,
141
+ fix: 'Ensure the ref file contains valid JSON with { "hash": "sha256:<64 hex chars>", "invariants": ["..."] }.',
142
+ details: { path: filePath, reason },
143
+ });
144
+ }
145
+
134
146
  export function errorInvalidRefName(refName: string): MigrationToolsError {
135
147
  return new MigrationToolsError('MIGRATION.INVALID_REF_NAME', 'Invalid ref name', {
136
148
  why: `Ref name "${refName}" is invalid. Names must be lowercase alphanumeric with hyphens or forward slashes (no "." or ".." segments).`,
@@ -139,16 +151,12 @@ export function errorInvalidRefName(refName: string): MigrationToolsError {
139
151
  });
140
152
  }
141
153
 
142
- export function errorNoResolvableLeaf(reachableNodes: readonly string[]): MigrationToolsError {
143
- return new MigrationToolsError(
144
- 'MIGRATION.NO_RESOLVABLE_LEAF',
145
- 'Migration graph has no resolvable leaf',
146
- {
147
- why: `The migration graph contains cycles and no node has zero outgoing edges (reachable nodes: ${reachableNodes.join(', ')}). This typically happens after rollback migrations (e.g., C1→C2→C1).`,
148
- fix: 'Use --from <hash> to specify the planning origin explicitly.',
149
- details: { reachableNodes },
150
- },
151
- );
154
+ export function errorNoTarget(reachableHashes: readonly string[]): MigrationToolsError {
155
+ return new MigrationToolsError('MIGRATION.NO_TARGET', 'No migration target could be resolved', {
156
+ why: `The migration history contains cycles and no target can be resolved automatically (reachable hashes: ${reachableHashes.join(', ')}). This typically happens after rollback migrations (e.g., C1→C2→C1).`,
157
+ fix: 'Use --from <hash> to specify the planning origin explicitly.',
158
+ details: { reachableHashes },
159
+ });
152
160
  }
153
161
 
154
162
  export function errorInvalidRefValue(value: string): MigrationToolsError {
@@ -0,0 +1 @@
1
+ export { EMPTY_CONTRACT_HASH } from '../constants';