@prisma-next/migration-tools 0.0.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 +88 -0
- package/dist/errors-DdSjGRqx.mjs +115 -0
- package/dist/errors-DdSjGRqx.mjs.map +1 -0
- package/dist/exports/attestation.d.mts +15 -0
- package/dist/exports/attestation.d.mts.map +1 -0
- package/dist/exports/attestation.mjs +65 -0
- package/dist/exports/attestation.mjs.map +1 -0
- package/dist/exports/dag.d.mts +30 -0
- package/dist/exports/dag.d.mts.map +1 -0
- package/dist/exports/dag.mjs +182 -0
- package/dist/exports/dag.mjs.map +1 -0
- package/dist/exports/io.d.mts +10 -0
- package/dist/exports/io.d.mts.map +1 -0
- package/dist/exports/io.mjs +3 -0
- package/dist/exports/types.d.mts +35 -0
- package/dist/exports/types.d.mts.map +1 -0
- package/dist/exports/types.mjs +3 -0
- package/dist/io-Dx98-h0p.mjs +131 -0
- package/dist/io-Dx98-h0p.mjs.map +1 -0
- package/dist/types-CUnzoaLY.d.mts +56 -0
- package/dist/types-CUnzoaLY.d.mts.map +1 -0
- package/package.json +62 -0
- package/src/attestation.ts +76 -0
- package/src/canonicalize-json.ts +17 -0
- package/src/dag.ts +280 -0
- package/src/errors.ts +121 -0
- package/src/exports/attestation.ts +1 -0
- package/src/exports/dag.ts +8 -0
- package/src/exports/io.ts +6 -0
- package/src/exports/types.ts +9 -0
- package/src/io.ts +197 -0
- package/src/types.ts +51 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { a as errorInvalidJson, c as errorMissingFile, o as errorInvalidManifest, r as errorDirectoryExists, s as errorInvalidSlug } from "./errors-DdSjGRqx.mjs";
|
|
2
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { type } from "arktype";
|
|
4
|
+
import { basename, dirname, join } from "pathe";
|
|
5
|
+
|
|
6
|
+
//#region src/io.ts
|
|
7
|
+
const MANIFEST_FILE = "migration.json";
|
|
8
|
+
const OPS_FILE = "ops.json";
|
|
9
|
+
const MAX_SLUG_LENGTH = 64;
|
|
10
|
+
function hasErrnoCode(error, code) {
|
|
11
|
+
return error instanceof Error && error.code === code;
|
|
12
|
+
}
|
|
13
|
+
const MigrationManifestSchema = type({
|
|
14
|
+
from: "string",
|
|
15
|
+
to: "string",
|
|
16
|
+
migrationId: "string | null",
|
|
17
|
+
parentMigrationId: "string | null",
|
|
18
|
+
kind: "'regular' | 'baseline'",
|
|
19
|
+
fromContract: "object | null",
|
|
20
|
+
toContract: "object",
|
|
21
|
+
hints: type({
|
|
22
|
+
used: "string[]",
|
|
23
|
+
applied: "string[]",
|
|
24
|
+
plannerVersion: "string",
|
|
25
|
+
planningStrategy: "string"
|
|
26
|
+
}),
|
|
27
|
+
labels: "string[]",
|
|
28
|
+
"authorship?": type({
|
|
29
|
+
"author?": "string",
|
|
30
|
+
"email?": "string"
|
|
31
|
+
}),
|
|
32
|
+
"signature?": type({
|
|
33
|
+
keyId: "string",
|
|
34
|
+
value: "string"
|
|
35
|
+
}).or("null"),
|
|
36
|
+
createdAt: "string"
|
|
37
|
+
});
|
|
38
|
+
const MigrationOpsSchema = type({
|
|
39
|
+
id: "string",
|
|
40
|
+
label: "string",
|
|
41
|
+
operationClass: "'additive' | 'widening' | 'destructive'"
|
|
42
|
+
}).array();
|
|
43
|
+
async function writeMigrationPackage(dir, manifest, ops) {
|
|
44
|
+
await mkdir(dirname(dir), { recursive: true });
|
|
45
|
+
try {
|
|
46
|
+
await mkdir(dir);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (hasErrnoCode(error, "EEXIST")) throw errorDirectoryExists(dir);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2), { flag: "wx" });
|
|
52
|
+
await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: "wx" });
|
|
53
|
+
}
|
|
54
|
+
async function readMigrationPackage(dir) {
|
|
55
|
+
const manifestPath = join(dir, MANIFEST_FILE);
|
|
56
|
+
const opsPath = join(dir, OPS_FILE);
|
|
57
|
+
let manifestRaw;
|
|
58
|
+
try {
|
|
59
|
+
manifestRaw = await readFile(manifestPath, "utf-8");
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(MANIFEST_FILE, dir);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
let opsRaw;
|
|
65
|
+
try {
|
|
66
|
+
opsRaw = await readFile(opsPath, "utf-8");
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(OPS_FILE, dir);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
let manifest;
|
|
72
|
+
try {
|
|
73
|
+
manifest = JSON.parse(manifestRaw);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));
|
|
76
|
+
}
|
|
77
|
+
let ops;
|
|
78
|
+
try {
|
|
79
|
+
ops = JSON.parse(opsRaw);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));
|
|
82
|
+
}
|
|
83
|
+
validateManifest(manifest, manifestPath);
|
|
84
|
+
validateOps(ops, opsPath);
|
|
85
|
+
return {
|
|
86
|
+
dirName: basename(dir),
|
|
87
|
+
dirPath: dir,
|
|
88
|
+
manifest,
|
|
89
|
+
ops
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function validateManifest(manifest, filePath) {
|
|
93
|
+
const result = MigrationManifestSchema(manifest);
|
|
94
|
+
if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
|
|
95
|
+
}
|
|
96
|
+
function validateOps(ops, filePath) {
|
|
97
|
+
const result = MigrationOpsSchema(ops);
|
|
98
|
+
if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
|
|
99
|
+
}
|
|
100
|
+
async function readMigrationsDir(migrationsRoot) {
|
|
101
|
+
let entries;
|
|
102
|
+
try {
|
|
103
|
+
entries = await readdir(migrationsRoot);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (hasErrnoCode(error, "ENOENT")) return [];
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
const packages = [];
|
|
109
|
+
for (const entry of entries.sort()) {
|
|
110
|
+
const entryPath = join(migrationsRoot, entry);
|
|
111
|
+
if (!(await stat(entryPath)).isDirectory()) continue;
|
|
112
|
+
const manifestPath = join(entryPath, MANIFEST_FILE);
|
|
113
|
+
try {
|
|
114
|
+
await stat(manifestPath);
|
|
115
|
+
} catch {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
packages.push(await readMigrationPackage(entryPath));
|
|
119
|
+
}
|
|
120
|
+
return packages;
|
|
121
|
+
}
|
|
122
|
+
function formatMigrationDirName(timestamp, slug) {
|
|
123
|
+
const sanitized = slug.toLowerCase().replace(/[^a-z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
124
|
+
if (sanitized.length === 0) throw errorInvalidSlug(slug);
|
|
125
|
+
const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);
|
|
126
|
+
return `${timestamp.getUTCFullYear()}${String(timestamp.getUTCMonth() + 1).padStart(2, "0")}${String(timestamp.getUTCDate()).padStart(2, "0")}T${String(timestamp.getUTCHours()).padStart(2, "0")}${String(timestamp.getUTCMinutes()).padStart(2, "0")}_${truncated}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
//#endregion
|
|
130
|
+
export { writeMigrationPackage as i, readMigrationPackage as n, readMigrationsDir as r, formatMigrationDirName as t };
|
|
131
|
+
//# sourceMappingURL=io-Dx98-h0p.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"io-Dx98-h0p.mjs","names":["manifestRaw: string","opsRaw: string","manifest: MigrationManifest","ops: MigrationOps","entries: string[]","packages: MigrationPackage[]"],"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 { MigrationManifest, MigrationOps, MigrationPackage } 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 parentMigrationId: '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'\",\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 readMigrationPackage(dir: string): Promise<MigrationPackage> {\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 MigrationPackage[]> {\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: MigrationPackage[] = [];\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,56 @@
|
|
|
1
|
+
import { ContractIR } from "@prisma-next/contract/ir";
|
|
2
|
+
import { MigrationPlanOperation } from "@prisma-next/core-control-plane/types";
|
|
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
|
+
readonly planningStrategy: string;
|
|
10
|
+
}
|
|
11
|
+
interface MigrationManifest {
|
|
12
|
+
readonly from: string;
|
|
13
|
+
readonly to: string;
|
|
14
|
+
readonly migrationId: string | null;
|
|
15
|
+
readonly parentMigrationId: string | null;
|
|
16
|
+
readonly kind: 'regular' | 'baseline';
|
|
17
|
+
readonly fromContract: ContractIR | null;
|
|
18
|
+
readonly toContract: ContractIR;
|
|
19
|
+
readonly hints: MigrationHints;
|
|
20
|
+
readonly labels: readonly string[];
|
|
21
|
+
readonly authorship?: {
|
|
22
|
+
readonly author?: string;
|
|
23
|
+
readonly email?: string;
|
|
24
|
+
};
|
|
25
|
+
readonly signature?: {
|
|
26
|
+
readonly keyId: string;
|
|
27
|
+
readonly value: string;
|
|
28
|
+
} | null;
|
|
29
|
+
readonly createdAt: string;
|
|
30
|
+
}
|
|
31
|
+
type MigrationOps = readonly MigrationPlanOperation[];
|
|
32
|
+
interface MigrationPackage {
|
|
33
|
+
readonly dirName: string;
|
|
34
|
+
readonly dirPath: string;
|
|
35
|
+
readonly manifest: MigrationManifest;
|
|
36
|
+
readonly ops: MigrationOps;
|
|
37
|
+
}
|
|
38
|
+
interface MigrationChainEntry {
|
|
39
|
+
readonly from: string;
|
|
40
|
+
readonly to: string;
|
|
41
|
+
readonly migrationId: string | null;
|
|
42
|
+
readonly parentMigrationId: string | null;
|
|
43
|
+
readonly dirName: string;
|
|
44
|
+
readonly createdAt: string;
|
|
45
|
+
readonly labels: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
interface MigrationGraph {
|
|
48
|
+
readonly nodes: ReadonlySet<string>;
|
|
49
|
+
readonly forwardChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;
|
|
50
|
+
readonly reverseChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;
|
|
51
|
+
readonly migrationById: ReadonlyMap<string, MigrationChainEntry>;
|
|
52
|
+
readonly childrenByParentId: ReadonlyMap<string | null, readonly MigrationChainEntry[]>;
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { MigrationOps as a, MigrationManifest as i, MigrationGraph as n, MigrationPackage as o, MigrationHints as r, MigrationChainEntry as t };
|
|
56
|
+
//# sourceMappingURL=types-CUnzoaLY.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-CUnzoaLY.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;UAGiB,cAAA;;EAAA,SAAA,OAAA,EAAc,SAAA,MAAA,EAAA;EAOd,SAAA,cAAiB,EAAA,MAAA;EAMT,SAAA,gBAAA,EAAA,MAAA;;AAEP,UARD,iBAAA,CAQC;EAAc,SAAA,IAAA,EAAA,MAAA;EAOpB,SAAA,EAAA,EAAA,MAAY;EAEP,SAAA,WAAgB,EAAA,MAAA,GAGZ,IAAA;EAIJ,SAAA,iBAAmB,EAAA,MAAA,GAAA,IAAA;EAUnB,SAAA,IAAA,EAAA,SAAc,GAAA,UAAA;EACb,SAAA,YAAA,EA7BO,UA6BP,GAAA,IAAA;EACoC,SAAA,UAAA,EA7B/B,UA6B+B;EAA7B,SAAA,KAAA,EA5BP,cA4BO;EAC6B,SAAA,MAAA,EAAA,SAAA,MAAA,EAAA;EAA7B,SAAA,UAAA,CAAA,EAAA;IACqB,SAAA,MAAA,CAAA,EAAA,MAAA;IAApB,SAAA,KAAA,CAAA,EAAA,MAAA;EACyC,CAAA;EAApC,SAAA,SAAA,CAAA,EAAA;IAAW,SAAA,KAAA,EAAA,MAAA;;;;;KAxB9B,YAAA,YAAwB;UAEnB,gBAAA;;;qBAGI;gBACL;;UAGC,mBAAA;;;;;;;;;UAUA,cAAA;kBACC;yBACO,6BAA6B;yBAC7B,6BAA6B;0BAC5B,oBAAoB;+BACf,oCAAoC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@prisma-next/migration-tools",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"description": "On-disk migration persistence, attestation, and chain reconstruction for Prisma Next",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsdown",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"test:coverage": "vitest run --coverage",
|
|
11
|
+
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
12
|
+
"lint": "biome check . --error-on-warnings",
|
|
13
|
+
"lint:fix": "biome check --write .",
|
|
14
|
+
"lint:fix:unsafe": "biome check --write --unsafe .",
|
|
15
|
+
"clean": "rm -rf dist dist-tsc dist-tsc-prod coverage .tmp-output"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@prisma-next/contract": "workspace:*",
|
|
19
|
+
"@prisma-next/core-control-plane": "workspace:*",
|
|
20
|
+
"@prisma-next/utils": "workspace:*",
|
|
21
|
+
"arktype": "catalog:",
|
|
22
|
+
"pathe": "^2.0.3"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@prisma-next/tsconfig": "workspace:*",
|
|
26
|
+
"@prisma-next/tsdown": "workspace:*",
|
|
27
|
+
"tsdown": "catalog:",
|
|
28
|
+
"typescript": "catalog:",
|
|
29
|
+
"vitest": "catalog:"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"src"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"exports": {
|
|
39
|
+
"./types": {
|
|
40
|
+
"types": "./dist/exports/types.d.mts",
|
|
41
|
+
"import": "./dist/exports/types.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./io": {
|
|
44
|
+
"types": "./dist/exports/io.d.mts",
|
|
45
|
+
"import": "./dist/exports/io.mjs"
|
|
46
|
+
},
|
|
47
|
+
"./attestation": {
|
|
48
|
+
"types": "./dist/exports/attestation.d.mts",
|
|
49
|
+
"import": "./dist/exports/attestation.mjs"
|
|
50
|
+
},
|
|
51
|
+
"./dag": {
|
|
52
|
+
"types": "./dist/exports/dag.d.mts",
|
|
53
|
+
"import": "./dist/exports/dag.mjs"
|
|
54
|
+
},
|
|
55
|
+
"./package.json": "./package.json"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "https://github.com/prisma/prisma-next.git",
|
|
60
|
+
"directory": "packages/1-framework/3-tooling/migration"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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';
|
|
5
|
+
import { canonicalizeJson } from './canonicalize-json';
|
|
6
|
+
import { readMigrationPackage } from './io';
|
|
7
|
+
import type { MigrationManifest, MigrationOps } from './types';
|
|
8
|
+
|
|
9
|
+
export interface VerifyResult {
|
|
10
|
+
readonly ok: boolean;
|
|
11
|
+
readonly reason?: 'draft' | 'mismatch';
|
|
12
|
+
readonly storedMigrationId?: string;
|
|
13
|
+
readonly computedMigrationId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sha256Hex(input: string): string {
|
|
17
|
+
return createHash('sha256').update(input).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function computeMigrationId(manifest: MigrationManifest, ops: MigrationOps): string {
|
|
21
|
+
const {
|
|
22
|
+
migrationId: _migrationId,
|
|
23
|
+
signature: _signature,
|
|
24
|
+
fromContract: _fromContract,
|
|
25
|
+
toContract: _toContract,
|
|
26
|
+
...strippedMeta
|
|
27
|
+
} = manifest;
|
|
28
|
+
|
|
29
|
+
const canonicalManifest = canonicalizeJson(strippedMeta);
|
|
30
|
+
const canonicalOps = canonicalizeJson(ops);
|
|
31
|
+
|
|
32
|
+
const canonicalFromContract =
|
|
33
|
+
manifest.fromContract !== null ? canonicalizeContract(manifest.fromContract) : 'null';
|
|
34
|
+
const canonicalToContract = canonicalizeContract(manifest.toContract);
|
|
35
|
+
|
|
36
|
+
const partHashes = [
|
|
37
|
+
canonicalManifest,
|
|
38
|
+
canonicalOps,
|
|
39
|
+
canonicalFromContract,
|
|
40
|
+
canonicalToContract,
|
|
41
|
+
].map(sha256Hex);
|
|
42
|
+
const hash = sha256Hex(canonicalizeJson(partHashes));
|
|
43
|
+
|
|
44
|
+
return `sha256:${hash}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function attestMigration(dir: string): Promise<string> {
|
|
48
|
+
const pkg = await readMigrationPackage(dir);
|
|
49
|
+
const migrationId = computeMigrationId(pkg.manifest, pkg.ops);
|
|
50
|
+
|
|
51
|
+
const updated = { ...pkg.manifest, migrationId };
|
|
52
|
+
await writeFile(join(dir, 'migration.json'), JSON.stringify(updated, null, 2));
|
|
53
|
+
|
|
54
|
+
return migrationId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function verifyMigration(dir: string): Promise<VerifyResult> {
|
|
58
|
+
const pkg = await readMigrationPackage(dir);
|
|
59
|
+
|
|
60
|
+
if (pkg.manifest.migrationId === null) {
|
|
61
|
+
return { ok: false, reason: 'draft' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const computed = computeMigrationId(pkg.manifest, pkg.ops);
|
|
65
|
+
|
|
66
|
+
if (pkg.manifest.migrationId === computed) {
|
|
67
|
+
return { ok: true, storedMigrationId: pkg.manifest.migrationId, computedMigrationId: computed };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
reason: 'mismatch',
|
|
73
|
+
storedMigrationId: pkg.manifest.migrationId,
|
|
74
|
+
computedMigrationId: computed,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function sortKeys(value: unknown): unknown {
|
|
2
|
+
if (value === null || typeof value !== 'object') {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
return value.map(sortKeys);
|
|
7
|
+
}
|
|
8
|
+
const sorted: Record<string, unknown> = {};
|
|
9
|
+
for (const key of Object.keys(value).sort()) {
|
|
10
|
+
sorted[key] = sortKeys((value as Record<string, unknown>)[key]);
|
|
11
|
+
}
|
|
12
|
+
return sorted;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function canonicalizeJson(value: unknown): string {
|
|
16
|
+
return JSON.stringify(sortKeys(value));
|
|
17
|
+
}
|
package/src/dag.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { EMPTY_CONTRACT_HASH } from '@prisma-next/core-control-plane/constants';
|
|
2
|
+
import {
|
|
3
|
+
errorAmbiguousLeaf,
|
|
4
|
+
errorDuplicateMigrationId,
|
|
5
|
+
errorNoRoot,
|
|
6
|
+
errorSelfLoop,
|
|
7
|
+
} from './errors';
|
|
8
|
+
import type { MigrationChainEntry, MigrationGraph, MigrationPackage } from './types';
|
|
9
|
+
|
|
10
|
+
export function reconstructGraph(packages: readonly MigrationPackage[]): MigrationGraph {
|
|
11
|
+
const nodes = new Set<string>();
|
|
12
|
+
const forwardChain = new Map<string, MigrationChainEntry[]>();
|
|
13
|
+
const reverseChain = new Map<string, MigrationChainEntry[]>();
|
|
14
|
+
const migrationById = new Map<string, MigrationChainEntry>();
|
|
15
|
+
const childrenByParentId = new Map<string | null, MigrationChainEntry[]>();
|
|
16
|
+
|
|
17
|
+
for (const pkg of packages) {
|
|
18
|
+
const { from, to } = pkg.manifest;
|
|
19
|
+
|
|
20
|
+
if (from === to) {
|
|
21
|
+
throw errorSelfLoop(pkg.dirName, from);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
nodes.add(from);
|
|
25
|
+
nodes.add(to);
|
|
26
|
+
|
|
27
|
+
const migration: MigrationChainEntry = {
|
|
28
|
+
from,
|
|
29
|
+
to,
|
|
30
|
+
migrationId: pkg.manifest.migrationId,
|
|
31
|
+
parentMigrationId: pkg.manifest.parentMigrationId,
|
|
32
|
+
dirName: pkg.dirName,
|
|
33
|
+
createdAt: pkg.manifest.createdAt,
|
|
34
|
+
labels: pkg.manifest.labels,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (migration.migrationId !== null) {
|
|
38
|
+
if (migrationById.has(migration.migrationId)) {
|
|
39
|
+
throw errorDuplicateMigrationId(migration.migrationId);
|
|
40
|
+
}
|
|
41
|
+
migrationById.set(migration.migrationId, migration);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const parentId = migration.parentMigrationId;
|
|
45
|
+
const siblings = childrenByParentId.get(parentId);
|
|
46
|
+
if (siblings) {
|
|
47
|
+
siblings.push(migration);
|
|
48
|
+
} else {
|
|
49
|
+
childrenByParentId.set(parentId, [migration]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fwd = forwardChain.get(from);
|
|
53
|
+
if (fwd) {
|
|
54
|
+
fwd.push(migration);
|
|
55
|
+
} else {
|
|
56
|
+
forwardChain.set(from, [migration]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rev = reverseChain.get(to);
|
|
60
|
+
if (rev) {
|
|
61
|
+
rev.push(migration);
|
|
62
|
+
} else {
|
|
63
|
+
reverseChain.set(to, [migration]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { nodes, forwardChain, reverseChain, migrationById, childrenByParentId };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Walk the parent-migration chain to find the latest migration.
|
|
72
|
+
* Returns the migration with no children, or null for an empty graph.
|
|
73
|
+
* Throws AMBIGUOUS_LEAF if the chain branches.
|
|
74
|
+
*/
|
|
75
|
+
export function findLatestMigration(graph: MigrationGraph): MigrationChainEntry | null {
|
|
76
|
+
if (graph.nodes.size === 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const roots = graph.childrenByParentId.get(null);
|
|
81
|
+
if (!roots || roots.length === 0) {
|
|
82
|
+
throw errorNoRoot([...graph.nodes].sort());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (roots.length > 1) {
|
|
86
|
+
throw errorAmbiguousLeaf(roots.map((e) => e.to));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let current = roots[0];
|
|
90
|
+
if (!current) {
|
|
91
|
+
throw errorNoRoot([...graph.nodes].sort());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (let depth = 0; depth < graph.migrationById.size + 1 && current; depth++) {
|
|
95
|
+
const children: readonly MigrationChainEntry[] | undefined =
|
|
96
|
+
current.migrationId !== null ? graph.childrenByParentId.get(current.migrationId) : undefined;
|
|
97
|
+
|
|
98
|
+
if (!children || children.length === 0) {
|
|
99
|
+
return current;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (children.length > 1) {
|
|
103
|
+
throw errorAmbiguousLeaf(children.map((e) => e.to));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
current = children[0];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw errorNoRoot([...graph.nodes].sort());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Find the leaf contract hash of the migration chain.
|
|
114
|
+
* Convenience wrapper around findLatestMigration.
|
|
115
|
+
*/
|
|
116
|
+
export function findLeaf(graph: MigrationGraph): string {
|
|
117
|
+
const migration = findLatestMigration(graph);
|
|
118
|
+
return migration ? migration.to : EMPTY_CONTRACT_HASH;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Find the ordered chain of migrations from `fromHash` to `toHash` by walking the
|
|
123
|
+
* parent-migration chain. Returns the sub-sequence of migrations whose cumulative path
|
|
124
|
+
* goes from `fromHash` to `toHash`.
|
|
125
|
+
*
|
|
126
|
+
* This reconstructs the full chain from root to leaf via parent pointers, then
|
|
127
|
+
* extracts the segment between the two hashes. This correctly handles revisited
|
|
128
|
+
* contract hashes (e.g. A→B→A) because it operates on migrations, not nodes.
|
|
129
|
+
*/
|
|
130
|
+
export function findPath(
|
|
131
|
+
graph: MigrationGraph,
|
|
132
|
+
fromHash: string,
|
|
133
|
+
toHash: string,
|
|
134
|
+
): readonly MigrationChainEntry[] | null {
|
|
135
|
+
if (fromHash === toHash) return [];
|
|
136
|
+
|
|
137
|
+
const chain = buildChain(graph);
|
|
138
|
+
if (!chain) return null;
|
|
139
|
+
|
|
140
|
+
let startIdx = -1;
|
|
141
|
+
if (chain.length > 0 && chain[0]?.from === fromHash) {
|
|
142
|
+
startIdx = 0;
|
|
143
|
+
} else {
|
|
144
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
145
|
+
if (chain[i]?.to === fromHash) {
|
|
146
|
+
startIdx = i + 1;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (startIdx === -1) return null;
|
|
153
|
+
|
|
154
|
+
let endIdx = -1;
|
|
155
|
+
for (let i = chain.length - 1; i >= startIdx; i--) {
|
|
156
|
+
if (chain[i]?.to === toHash) {
|
|
157
|
+
endIdx = i + 1;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (endIdx === -1) return null;
|
|
163
|
+
|
|
164
|
+
return chain.slice(startIdx, endIdx);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build the full ordered chain of migrations from root to leaf by following
|
|
169
|
+
* parent pointers. Returns null if the chain cannot be reconstructed
|
|
170
|
+
* (e.g. missing root, branches).
|
|
171
|
+
*/
|
|
172
|
+
function buildChain(graph: MigrationGraph): readonly MigrationChainEntry[] | null {
|
|
173
|
+
const roots = graph.childrenByParentId.get(null);
|
|
174
|
+
if (!roots || roots.length !== 1) return null;
|
|
175
|
+
|
|
176
|
+
const chain: MigrationChainEntry[] = [];
|
|
177
|
+
let current: MigrationChainEntry | undefined = roots[0];
|
|
178
|
+
|
|
179
|
+
for (let depth = 0; depth < graph.migrationById.size + 1 && current; depth++) {
|
|
180
|
+
chain.push(current);
|
|
181
|
+
const children =
|
|
182
|
+
current.migrationId !== null ? graph.childrenByParentId.get(current.migrationId) : undefined;
|
|
183
|
+
if (!children || children.length === 0) break;
|
|
184
|
+
if (children.length > 1) return null;
|
|
185
|
+
current = children[0];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return chain;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function detectCycles(graph: MigrationGraph): readonly string[][] {
|
|
192
|
+
const WHITE = 0;
|
|
193
|
+
const GRAY = 1;
|
|
194
|
+
const BLACK = 2;
|
|
195
|
+
|
|
196
|
+
const color = new Map<string, number>();
|
|
197
|
+
const parent = new Map<string, string | null>();
|
|
198
|
+
const cycles: string[][] = [];
|
|
199
|
+
|
|
200
|
+
for (const node of graph.nodes) {
|
|
201
|
+
color.set(node, WHITE);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function dfs(u: string): void {
|
|
205
|
+
color.set(u, GRAY);
|
|
206
|
+
|
|
207
|
+
const outgoing = graph.forwardChain.get(u);
|
|
208
|
+
if (outgoing) {
|
|
209
|
+
for (const edge of outgoing) {
|
|
210
|
+
const v = edge.to;
|
|
211
|
+
if (color.get(v) === GRAY) {
|
|
212
|
+
// Back edge found — reconstruct cycle
|
|
213
|
+
const cycle: string[] = [v];
|
|
214
|
+
let cur = u;
|
|
215
|
+
while (cur !== v) {
|
|
216
|
+
cycle.push(cur);
|
|
217
|
+
cur = parent.get(cur) ?? v;
|
|
218
|
+
}
|
|
219
|
+
cycle.reverse();
|
|
220
|
+
cycles.push(cycle);
|
|
221
|
+
} else if (color.get(v) === WHITE) {
|
|
222
|
+
parent.set(v, u);
|
|
223
|
+
dfs(v);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
color.set(u, BLACK);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const node of graph.nodes) {
|
|
232
|
+
if (color.get(node) === WHITE) {
|
|
233
|
+
parent.set(node, null);
|
|
234
|
+
dfs(node);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return cycles;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function detectOrphans(graph: MigrationGraph): readonly MigrationChainEntry[] {
|
|
242
|
+
if (graph.nodes.size === 0) return [];
|
|
243
|
+
|
|
244
|
+
const reachable = new Set<string>();
|
|
245
|
+
const rootMigrations = graph.childrenByParentId.get(null) ?? [];
|
|
246
|
+
const emptyRootExists = rootMigrations.some(
|
|
247
|
+
(migration) => migration.from === EMPTY_CONTRACT_HASH,
|
|
248
|
+
);
|
|
249
|
+
const rootHashes = emptyRootExists
|
|
250
|
+
? [EMPTY_CONTRACT_HASH]
|
|
251
|
+
: [...new Set(rootMigrations.map((migration) => migration.from))];
|
|
252
|
+
const queue: string[] = rootHashes.length > 0 ? rootHashes : [EMPTY_CONTRACT_HASH];
|
|
253
|
+
|
|
254
|
+
for (const hash of queue) {
|
|
255
|
+
reachable.add(hash);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
while (queue.length > 0) {
|
|
259
|
+
const node = queue.shift();
|
|
260
|
+
if (node === undefined) break;
|
|
261
|
+
const outgoing = graph.forwardChain.get(node);
|
|
262
|
+
if (!outgoing) continue;
|
|
263
|
+
|
|
264
|
+
for (const migration of outgoing) {
|
|
265
|
+
if (!reachable.has(migration.to)) {
|
|
266
|
+
reachable.add(migration.to);
|
|
267
|
+
queue.push(migration.to);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const orphans: MigrationChainEntry[] = [];
|
|
273
|
+
for (const [from, migrations] of graph.forwardChain) {
|
|
274
|
+
if (!reachable.has(from)) {
|
|
275
|
+
orphans.push(...migrations);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return orphans;
|
|
280
|
+
}
|