@prisma-next/cli 0.16.0-dev.4 → 0.16.0-dev.5
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 +3 -4
- package/dist/cli.mjs +6 -6
- package/dist/commands/db-sign.d.mts.map +1 -1
- package/dist/commands/db-sign.mjs +4 -6
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.d.mts.map +1 -1
- package/dist/commands/db-update.mjs +5 -8
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/migrate.mjs +2 -2
- package/dist/commands/migration-check.d.mts +3 -1
- package/dist/commands/migration-check.d.mts.map +1 -1
- package/dist/commands/migration-check.mjs +1 -1
- package/dist/commands/migration-graph.mjs +1 -1
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +1 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +12 -36
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-status.mjs +1 -1
- package/dist/commands/ref.d.mts.map +1 -1
- package/dist/commands/ref.mjs +7 -9
- package/dist/commands/ref.mjs.map +1 -1
- package/dist/{contract-at-errors-D9gEjYtg.mjs → contract-at-errors-CTtzKR6q.mjs} +6 -10
- package/dist/contract-at-errors-CTtzKR6q.mjs.map +1 -0
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{init-C3JhkHg-.mjs → init-CgKEi_t5.mjs} +53 -45
- package/dist/init-CgKEi_t5.mjs.map +1 -0
- package/dist/{migration-check-MHT3w7_v.mjs → migration-check-CcuayFB2.mjs} +49 -22
- package/dist/migration-check-CcuayFB2.mjs.map +1 -0
- package/dist/{migration-list-BAKujUjh.mjs → migration-list-6z6vuXHa.mjs} +3 -3
- package/dist/migration-list-6z6vuXHa.mjs.map +1 -0
- package/dist/{migration-plan-CreSVtWZ.mjs → migration-plan-DuMEUejG.mjs} +46 -54
- package/dist/migration-plan-DuMEUejG.mjs.map +1 -0
- package/dist/{migration-status-YgEvXf-u.mjs → migration-status-B61YOaMl.mjs} +3 -2
- package/dist/{migration-status-YgEvXf-u.mjs.map → migration-status-B61YOaMl.mjs.map} +1 -1
- package/dist/{output-mEQ74_nd.mjs → output-DWNYqZGc.mjs} +5 -6
- package/dist/output-DWNYqZGc.mjs.map +1 -0
- package/package.json +21 -21
- package/src/commands/db-sign.ts +6 -4
- package/src/commands/db-update.ts +13 -6
- package/src/commands/init/hygiene-gitattributes.ts +37 -21
- package/src/commands/init/init.ts +6 -6
- package/src/commands/init/output.ts +4 -5
- package/src/commands/init/reinit-cleanup.ts +16 -17
- package/src/commands/migration-check.ts +69 -32
- package/src/commands/migration-list.ts +1 -5
- package/src/commands/migration-new.ts +15 -39
- package/src/commands/migration-plan.ts +54 -76
- package/src/commands/ref.ts +18 -7
- package/src/utils/contract-at-errors.ts +9 -13
- package/src/utils/contract-space-seed-phase.ts +8 -6
- package/src/utils/plan-resolution.ts +1 -4
- package/dist/contract-at-errors-D9gEjYtg.mjs.map +0 -1
- package/dist/init-C3JhkHg-.mjs.map +0 -1
- package/dist/migration-check-MHT3w7_v.mjs.map +0 -1
- package/dist/migration-list-BAKujUjh.mjs.map +0 -1
- package/dist/migration-plan-CreSVtWZ.mjs.map +0 -1
- package/dist/output-mEQ74_nd.mjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync,
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { loadConfig } from '@prisma-next/config-loader';
|
|
4
4
|
import { createControlStack } from '@prisma-next/framework-components/control';
|
|
@@ -7,6 +7,11 @@ import type {
|
|
|
7
7
|
IntegrityViolation,
|
|
8
8
|
} from '@prisma-next/migration-tools/aggregate';
|
|
9
9
|
import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
|
|
10
|
+
import {
|
|
11
|
+
contractSnapshotDir,
|
|
12
|
+
readContractSnapshotJson,
|
|
13
|
+
} from '@prisma-next/migration-tools/contract-snapshot-store';
|
|
14
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
10
15
|
import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
|
|
11
16
|
import { verifyMigrationHash } from '@prisma-next/migration-tools/hash';
|
|
12
17
|
import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
|
|
@@ -18,7 +23,6 @@ import type { Refs } from '@prisma-next/migration-tools/refs';
|
|
|
18
23
|
import {
|
|
19
24
|
isValidSpaceId,
|
|
20
25
|
listContractSpaceDirectories,
|
|
21
|
-
RESERVED_SPACE_SUBDIR_NAMES,
|
|
22
26
|
spaceMigrationDirectory,
|
|
23
27
|
spaceRefsDirectory,
|
|
24
28
|
} from '@prisma-next/migration-tools/spaces';
|
|
@@ -92,32 +96,57 @@ function checkFileExists(
|
|
|
92
96
|
return null;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Snapshot-store consistency check for one migration package (D6.5):
|
|
101
|
+
* absent store entry is not an issue (runner independence, ADR 199 — a
|
|
102
|
+
* package dir with only `migration.json` + `ops.json` is legitimate); a
|
|
103
|
+
* present entry whose inner `storage.storageHash` disagrees with
|
|
104
|
+
* `pkg.metadata.to` is `MIGRATION.CHECK_SNAPSHOT_HASH_MISMATCH`; an unparseable store
|
|
105
|
+
* entry (or a malformed `to`) is `MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE`.
|
|
106
|
+
*/
|
|
107
|
+
async function checkSnapshotConsistency(
|
|
96
108
|
spaceId: string,
|
|
97
109
|
pkg: OnDiskMigrationPackage,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
migrationsDir: string,
|
|
111
|
+
): Promise<CheckFailure | null> {
|
|
112
|
+
let snapshotDir: string;
|
|
101
113
|
try {
|
|
102
|
-
|
|
103
|
-
const storage = raw['storage'] as Record<string, unknown> | undefined;
|
|
104
|
-
const snapshotHash = storage?.['storageHash'];
|
|
105
|
-
if (typeof snapshotHash === 'string' && snapshotHash !== pkg.metadata.to) {
|
|
106
|
-
return {
|
|
107
|
-
space: spaceId,
|
|
108
|
-
code: 'MIGRATION.CHECK_SNAPSHOT_HASH_MISMATCH',
|
|
109
|
-
where: migrationPathRelative(pkg.dirPath),
|
|
110
|
-
why: `Migration "${pkg.dirName}" declares to=${pkg.metadata.to} but end-contract.json has storageHash=${snapshotHash}`,
|
|
111
|
-
fix: 'Re-emit the migration package so migration.json and end-contract.json agree.',
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
+
snapshotDir = contractSnapshotDir(migrationsDir, pkg.metadata.to);
|
|
114
115
|
} catch {
|
|
115
116
|
return {
|
|
116
117
|
space: spaceId,
|
|
117
118
|
code: 'MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE',
|
|
118
119
|
where: migrationPathRelative(pkg.dirPath),
|
|
119
|
-
why: `Migration "${pkg.dirName}"
|
|
120
|
-
fix: 'Re-emit the migration package
|
|
120
|
+
why: `Migration "${pkg.dirName}" declares to="${pkg.metadata.to}", which is not a well-formed contract snapshot hash.`,
|
|
121
|
+
fix: 'Re-emit the migration package so migration.json declares a valid `sha256:<64hex>` to-hash.',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let raw: unknown;
|
|
126
|
+
try {
|
|
127
|
+
raw = await readContractSnapshotJson(migrationsDir, pkg.metadata.to);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (MigrationToolsError.is(error) && error.code === 'MIGRATION.CONTRACT_SNAPSHOT_MISSING') {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
space: spaceId,
|
|
134
|
+
code: 'MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE',
|
|
135
|
+
where: migrationPathRelative(pkg.dirPath),
|
|
136
|
+
why: `Migration "${pkg.dirName}" has an unparseable contract snapshot at ${snapshotDir}/contract.json.`,
|
|
137
|
+
fix: 'Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const record = raw !== null && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
|
|
141
|
+
const storage = record['storage'] as Record<string, unknown> | undefined;
|
|
142
|
+
const snapshotHash = storage?.['storageHash'];
|
|
143
|
+
if (typeof snapshotHash === 'string' && snapshotHash !== pkg.metadata.to) {
|
|
144
|
+
return {
|
|
145
|
+
space: spaceId,
|
|
146
|
+
code: 'MIGRATION.CHECK_SNAPSHOT_HASH_MISMATCH',
|
|
147
|
+
where: migrationPathRelative(pkg.dirPath),
|
|
148
|
+
why: `Migration "${pkg.dirName}" declares to=${pkg.metadata.to} but its contract snapshot has storageHash=${snapshotHash}`,
|
|
149
|
+
fix: 'Re-emit the migration package so migration.json and its contract snapshot agree.',
|
|
121
150
|
};
|
|
122
151
|
}
|
|
123
152
|
return null;
|
|
@@ -137,6 +166,8 @@ export interface CheckSpace {
|
|
|
137
166
|
readonly graph: MigrationGraph;
|
|
138
167
|
readonly migrationsDir: string;
|
|
139
168
|
readonly refsDir: string;
|
|
169
|
+
/** Migrations root the contract-snapshot store lives under — shared by every space. */
|
|
170
|
+
readonly projectMigrationsDir: string;
|
|
140
171
|
}
|
|
141
172
|
|
|
142
173
|
/**
|
|
@@ -152,9 +183,7 @@ export async function enumerateCheckSpaces(
|
|
|
152
183
|
projectMigrationsDir: string,
|
|
153
184
|
): Promise<readonly CheckSpace[]> {
|
|
154
185
|
const candidateDirs = await listContractSpaceDirectories(projectMigrationsDir);
|
|
155
|
-
const onDiskSpaceIds = new Set(
|
|
156
|
-
candidateDirs.filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name)).filter(isValidSpaceId),
|
|
157
|
-
);
|
|
186
|
+
const onDiskSpaceIds = new Set(candidateDirs.filter(isValidSpaceId));
|
|
158
187
|
const spaces: CheckSpace[] = [];
|
|
159
188
|
for (const space of aggregate.spaces()) {
|
|
160
189
|
const spaceId = space.spaceId;
|
|
@@ -168,6 +197,7 @@ export async function enumerateCheckSpaces(
|
|
|
168
197
|
graph: space.graph(),
|
|
169
198
|
migrationsDir,
|
|
170
199
|
refsDir: spaceRefsDirectory(migrationsDir),
|
|
200
|
+
projectMigrationsDir,
|
|
171
201
|
});
|
|
172
202
|
}
|
|
173
203
|
return spaces;
|
|
@@ -238,12 +268,15 @@ function checkDanglingRefs(space: CheckSpace): readonly CheckFailure[] {
|
|
|
238
268
|
return failures;
|
|
239
269
|
}
|
|
240
270
|
|
|
241
|
-
function checkSpace(space: CheckSpace): readonly CheckFailure[] {
|
|
271
|
+
async function checkSpace(space: CheckSpace): Promise<readonly CheckFailure[]> {
|
|
272
|
+
const snapshotFailures = await Promise.all(
|
|
273
|
+
space.packages.map((pkg) =>
|
|
274
|
+
checkSnapshotConsistency(space.spaceId, pkg, space.projectMigrationsDir),
|
|
275
|
+
),
|
|
276
|
+
);
|
|
242
277
|
return [
|
|
243
278
|
...checkManifestFilesPresent(space),
|
|
244
|
-
...
|
|
245
|
-
.map((pkg) => checkSnapshotConsistency(space.spaceId, pkg))
|
|
246
|
-
.filter((f): f is CheckFailure => f !== null),
|
|
279
|
+
...snapshotFailures.filter((f): f is CheckFailure => f !== null),
|
|
247
280
|
...checkReachability(space),
|
|
248
281
|
...checkDanglingRefs(space),
|
|
249
282
|
];
|
|
@@ -272,9 +305,9 @@ export interface RunMigrationCheckInputs {
|
|
|
272
305
|
* Aggregate-integrity violations (which already span every space) are folded
|
|
273
306
|
* in by the caller, not here.
|
|
274
307
|
*/
|
|
275
|
-
export function runMigrationCheck(
|
|
308
|
+
export async function runMigrationCheck(
|
|
276
309
|
inputs: RunMigrationCheckInputs,
|
|
277
|
-
): Result<MigrationCheckResult, CliStructuredError
|
|
310
|
+
): Promise<Result<MigrationCheckResult, CliStructuredError>> {
|
|
278
311
|
const { spaces, spaceFilter } = inputs;
|
|
279
312
|
|
|
280
313
|
if (spaceFilter !== undefined && !isValidSpaceId(spaceFilter)) {
|
|
@@ -287,7 +320,7 @@ export function runMigrationCheck(
|
|
|
287
320
|
const scopedSpaces =
|
|
288
321
|
spaceFilter !== undefined ? spaces.filter((s) => s.spaceId === spaceFilter) : spaces;
|
|
289
322
|
|
|
290
|
-
const failures = scopedSpaces.
|
|
323
|
+
const failures = (await Promise.all(scopedSpaces.map(checkSpace))).flat();
|
|
291
324
|
if (failures.length === 0) {
|
|
292
325
|
return ok({ ok: true, failures: [], summary: 'All checks passed' });
|
|
293
326
|
}
|
|
@@ -365,7 +398,7 @@ async function executeMigrationCheckCommand(
|
|
|
365
398
|
});
|
|
366
399
|
}
|
|
367
400
|
|
|
368
|
-
const checkResult = runMigrationCheck({
|
|
401
|
+
const checkResult = await runMigrationCheck({
|
|
369
402
|
spaces,
|
|
370
403
|
...(options.space !== undefined ? { spaceFilter: options.space } : {}),
|
|
371
404
|
});
|
|
@@ -558,7 +591,11 @@ async function checkSingleTarget(
|
|
|
558
591
|
});
|
|
559
592
|
}
|
|
560
593
|
|
|
561
|
-
const snapshotFailure = checkSnapshotConsistency(
|
|
594
|
+
const snapshotFailure = await checkSnapshotConsistency(
|
|
595
|
+
matchedSpace.spaceId,
|
|
596
|
+
matchedPkg,
|
|
597
|
+
matchedSpace.projectMigrationsDir,
|
|
598
|
+
);
|
|
562
599
|
if (snapshotFailure) failures.push(snapshotFailure);
|
|
563
600
|
|
|
564
601
|
const resolvedSpaceId = matchedSpace.spaceId !== 'app' ? matchedSpace.spaceId : undefined;
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
APP_SPACE_ID,
|
|
10
10
|
isValidSpaceId,
|
|
11
11
|
listContractSpaceDirectories,
|
|
12
|
-
RESERVED_SPACE_SUBDIR_NAMES,
|
|
13
12
|
} from '@prisma-next/migration-tools/spaces';
|
|
14
13
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
15
14
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
@@ -81,10 +80,7 @@ export function listRefsByContractHash(
|
|
|
81
80
|
|
|
82
81
|
async function orderedOnDiskSpaceIds(projectMigrationsDir: string): Promise<readonly string[]> {
|
|
83
82
|
const candidateDirs = await listContractSpaceDirectories(projectMigrationsDir);
|
|
84
|
-
return candidateDirs
|
|
85
|
-
.filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name))
|
|
86
|
-
.filter(isValidSpaceId)
|
|
87
|
-
.sort(compareSpaceIds);
|
|
83
|
+
return candidateDirs.filter(isValidSpaceId).sort(compareSpaceIds);
|
|
88
84
|
}
|
|
89
85
|
|
|
90
86
|
/**
|
|
@@ -14,12 +14,13 @@ import type { Contract } from '@prisma-next/contract/types';
|
|
|
14
14
|
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
15
15
|
import { APP_SPACE_ID, createControlStack } from '@prisma-next/framework-components/control';
|
|
16
16
|
import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
|
|
17
|
-
import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
|
|
18
17
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from '@prisma-next/migration-tools/
|
|
18
|
+
contractSnapshotDir,
|
|
19
|
+
snapshotsImportPathFrom,
|
|
20
|
+
writeContractSnapshot,
|
|
21
|
+
} from '@prisma-next/migration-tools/contract-snapshot-store';
|
|
22
|
+
import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
|
|
23
|
+
import { formatMigrationDirName, writeMigrationPackage } from '@prisma-next/migration-tools/io';
|
|
23
24
|
import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
|
|
24
25
|
import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
|
|
25
26
|
import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
@@ -28,7 +29,6 @@ import { Command } from 'commander';
|
|
|
28
29
|
import { join, relative } from 'pathe';
|
|
29
30
|
import {
|
|
30
31
|
CliStructuredError,
|
|
31
|
-
errorFileNotFound,
|
|
32
32
|
errorRuntime,
|
|
33
33
|
errorTargetMigrationNotSupported,
|
|
34
34
|
errorUnexpected,
|
|
@@ -133,7 +133,6 @@ async function executeMigrationNewCommand(
|
|
|
133
133
|
const graph = aggregate.app.graph();
|
|
134
134
|
|
|
135
135
|
let fromHash: string | null = null;
|
|
136
|
-
let fromContractSourceDir: string | null = null;
|
|
137
136
|
|
|
138
137
|
if (packages.length > 0) {
|
|
139
138
|
if (options.from) {
|
|
@@ -147,17 +146,10 @@ async function executeMigrationNewCommand(
|
|
|
147
146
|
);
|
|
148
147
|
}
|
|
149
148
|
fromHash = match.metadata.to;
|
|
150
|
-
fromContractSourceDir = match.dirPath;
|
|
151
149
|
} else {
|
|
152
150
|
const latestMigration = findLatestMigration(graph);
|
|
153
151
|
if (latestMigration) {
|
|
154
152
|
fromHash = latestMigration.to;
|
|
155
|
-
const leafPkg = packages.find(
|
|
156
|
-
(p) => p.metadata.migrationHash === latestMigration.migrationHash,
|
|
157
|
-
);
|
|
158
|
-
if (leafPkg) {
|
|
159
|
-
fromContractSourceDir = leafPkg.dirPath;
|
|
160
|
-
}
|
|
161
153
|
}
|
|
162
154
|
}
|
|
163
155
|
}
|
|
@@ -209,39 +201,23 @@ async function executeMigrationNewCommand(
|
|
|
209
201
|
|
|
210
202
|
await writeMigrationPackage(packageDir, metadata, []);
|
|
211
203
|
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
212
|
-
await
|
|
213
|
-
|
|
214
|
-
|
|
204
|
+
const [contractJsonRaw, contractDts] = await Promise.all([
|
|
205
|
+
readFile(destinationArtifacts.jsonPath, 'utf-8'),
|
|
206
|
+
readFile(destinationArtifacts.dtsPath, 'utf-8'),
|
|
215
207
|
]);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
await copyFilesWithRename(packageDir, [
|
|
222
|
-
{ sourcePath: sourceArtifacts.jsonPath, destName: 'start-contract.json' },
|
|
223
|
-
{ sourcePath: sourceArtifacts.dtsPath, destName: 'start-contract.d.ts' },
|
|
224
|
-
]);
|
|
225
|
-
} catch (error) {
|
|
226
|
-
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
227
|
-
return notOk(
|
|
228
|
-
errorFileNotFound(sourceArtifacts.jsonPath, {
|
|
229
|
-
why: `Predecessor migration is missing its destination contract snapshot at ${sourceArtifacts.jsonPath}`,
|
|
230
|
-
fix: 'Re-emit the predecessor migration (`prisma-next migration plan` from its source) so its sibling `end-contract.json` is restored, then re-run this command.',
|
|
231
|
-
}),
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
throw error;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
208
|
+
await writeContractSnapshot(migrationsDir, toStorageHash, {
|
|
209
|
+
contractJson: JSON.parse(contractJsonRaw) as unknown,
|
|
210
|
+
contractDts,
|
|
211
|
+
});
|
|
237
212
|
|
|
238
213
|
const planner = migrations.createPlanner(controlAdapter);
|
|
239
214
|
const emptyPlan = planner.emptyMigration(
|
|
240
215
|
{
|
|
241
216
|
packageDir,
|
|
242
|
-
contractJsonPath: join(
|
|
217
|
+
contractJsonPath: join(contractSnapshotDir(migrationsDir, toStorageHash), 'contract.json'),
|
|
243
218
|
fromHash,
|
|
244
219
|
toHash: toStorageHash,
|
|
220
|
+
snapshotsImportPath: snapshotsImportPathFrom(packageDir, migrationsDir),
|
|
245
221
|
},
|
|
246
222
|
APP_SPACE_ID,
|
|
247
223
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { loadConfig } from '@prisma-next/config-loader';
|
|
3
3
|
import type { Contract } from '@prisma-next/contract/types';
|
|
4
4
|
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
@@ -9,15 +9,14 @@ import {
|
|
|
9
9
|
type OperationPreview,
|
|
10
10
|
type SchemaOwnership,
|
|
11
11
|
} from '@prisma-next/framework-components/control';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
snapshotsImportPathFrom,
|
|
14
|
+
writeContractSnapshot,
|
|
15
|
+
} from '@prisma-next/migration-tools/contract-snapshot-store';
|
|
13
16
|
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
14
17
|
import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
|
|
15
18
|
import { deriveProvidedInvariants } from '@prisma-next/migration-tools/invariants';
|
|
16
|
-
import {
|
|
17
|
-
copyFilesWithRename,
|
|
18
|
-
formatMigrationDirName,
|
|
19
|
-
writeMigrationPackage,
|
|
20
|
-
} from '@prisma-next/migration-tools/io';
|
|
19
|
+
import { formatMigrationDirName, writeMigrationPackage } from '@prisma-next/migration-tools/io';
|
|
21
20
|
import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
|
|
22
21
|
import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
23
22
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
@@ -62,27 +61,6 @@ interface MigrationPlanOptions extends CommonCommandOptions {
|
|
|
62
61
|
readonly to?: string;
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
async function writeSnapshotContractArtifacts(
|
|
66
|
-
packageDir: string,
|
|
67
|
-
contractJson: unknown,
|
|
68
|
-
contractDts: string,
|
|
69
|
-
artifactBasename: 'start-contract' | 'end-contract',
|
|
70
|
-
): Promise<void> {
|
|
71
|
-
await mkdir(packageDir, { recursive: true });
|
|
72
|
-
const jsonContent = `${canonicalizeJson(contractJson)}\n`;
|
|
73
|
-
const dtsContent = contractDts.endsWith('\n') ? contractDts : `${contractDts}\n`;
|
|
74
|
-
await writeFile(join(packageDir, `${artifactBasename}.json`), jsonContent);
|
|
75
|
-
await writeFile(join(packageDir, `${artifactBasename}.d.ts`), dtsContent);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function writeSnapshotStartContract(
|
|
79
|
-
packageDir: string,
|
|
80
|
-
contractJson: unknown,
|
|
81
|
-
contractDts: string,
|
|
82
|
-
): Promise<void> {
|
|
83
|
-
await writeSnapshotContractArtifacts(packageDir, contractJson, contractDts, 'start-contract');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
64
|
type PlannerSuccess = {
|
|
87
65
|
readonly plannedOps: readonly MigrationPlanOperation[];
|
|
88
66
|
readonly migrationTsContent: string;
|
|
@@ -99,6 +77,7 @@ async function runPlannerLeg(
|
|
|
99
77
|
fromContract: Contract | null,
|
|
100
78
|
spaceId: string,
|
|
101
79
|
ownership: SchemaOwnership,
|
|
80
|
+
snapshotsImportPath: string,
|
|
102
81
|
): Promise<Result<PlannerSuccess, CliStructuredError>> {
|
|
103
82
|
const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
|
|
104
83
|
const plannerResult = planner.plan({
|
|
@@ -114,6 +93,7 @@ async function runPlannerLeg(
|
|
|
114
93
|
// space in the aggregate, so it stays a genuine drop; a table another
|
|
115
94
|
// space owns is never dropped.
|
|
116
95
|
ownership,
|
|
96
|
+
snapshotsImportPath,
|
|
117
97
|
});
|
|
118
98
|
if (plannerResult.kind === 'failure') {
|
|
119
99
|
return notOk(
|
|
@@ -309,14 +289,18 @@ async function executeMigrationPlanCommand(
|
|
|
309
289
|
let toStorageHash: string = rawStorageHash;
|
|
310
290
|
|
|
311
291
|
// When `--to <ref>` resolves a non-default destination, these carry its raw
|
|
312
|
-
// artifacts so the planned package's
|
|
313
|
-
// resolved target rather than copied from the emitted
|
|
292
|
+
// artifacts so the planned package's destination snapshot store entry is
|
|
293
|
+
// written from the resolved target rather than copied from the emitted
|
|
294
|
+
// `contract.json`.
|
|
314
295
|
let toArtifacts: { contractJson: unknown; contractDts: string } | null = null;
|
|
315
296
|
|
|
316
297
|
let fromContract: Contract | null = null;
|
|
317
298
|
let fromHash: string | null = null;
|
|
318
|
-
let
|
|
319
|
-
|
|
299
|
+
let snapshotStartContract: {
|
|
300
|
+
readonly fromHash: string;
|
|
301
|
+
readonly contractJson: unknown;
|
|
302
|
+
readonly contractDts: string;
|
|
303
|
+
} | null = null;
|
|
320
304
|
let isAutoBaseline = false;
|
|
321
305
|
|
|
322
306
|
const tolerantAggregateResult = await loadContractSpaceAggregateForCli({
|
|
@@ -346,12 +330,12 @@ async function executeMigrationPlanCommand(
|
|
|
346
330
|
case 'graph-node':
|
|
347
331
|
fromHash = resolutionResult.value.fromHash;
|
|
348
332
|
fromContract = resolutionResult.value.fromContract;
|
|
349
|
-
fromContractSourceDir = resolutionResult.value.sourceDir;
|
|
350
333
|
break;
|
|
351
334
|
case 'snapshot':
|
|
352
335
|
fromHash = resolutionResult.value.fromHash;
|
|
353
336
|
fromContract = resolutionResult.value.fromContract;
|
|
354
337
|
snapshotStartContract = {
|
|
338
|
+
fromHash: resolutionResult.value.fromHash,
|
|
355
339
|
contractJson: resolutionResult.value.contractJson,
|
|
356
340
|
contractDts: resolutionResult.value.contractDts,
|
|
357
341
|
};
|
|
@@ -360,6 +344,7 @@ async function executeMigrationPlanCommand(
|
|
|
360
344
|
fromHash = resolutionResult.value.fromHash;
|
|
361
345
|
fromContract = resolutionResult.value.fromContract;
|
|
362
346
|
snapshotStartContract = {
|
|
347
|
+
fromHash: resolutionResult.value.fromHash,
|
|
363
348
|
contractJson: resolutionResult.value.contractJson,
|
|
364
349
|
contractDts: resolutionResult.value.contractDts,
|
|
365
350
|
};
|
|
@@ -369,7 +354,7 @@ async function executeMigrationPlanCommand(
|
|
|
369
354
|
|
|
370
355
|
// `--to <ref>` swaps the planner destination to an arbitrary resolved
|
|
371
356
|
// contract (e.g. an ancestor / rollback target). The from-side resolution
|
|
372
|
-
// above is untouched; only the destination + its
|
|
357
|
+
// above is untouched; only the destination + its snapshot store entry
|
|
373
358
|
// change.
|
|
374
359
|
if (options.to !== undefined) {
|
|
375
360
|
const toResolution = await resolveToForPlan(options.to, {
|
|
@@ -386,11 +371,11 @@ async function executeMigrationPlanCommand(
|
|
|
386
371
|
};
|
|
387
372
|
}
|
|
388
373
|
|
|
389
|
-
// Phase 1 — seed: unconditionally re-emit per-space pinned
|
|
374
|
+
// Phase 1 — seed: unconditionally re-emit per-space pinned artifacts
|
|
390
375
|
// (contract.json / contract.d.ts / refs/head.json) and materialise any
|
|
391
376
|
// descriptor-shipped migration packages not yet on disk. Runs before
|
|
392
377
|
// the no-op check so that an extension bump alone (with no structural
|
|
393
|
-
// app-space change) still re-pins extension
|
|
378
|
+
// app-space change) still re-pins extension artifacts on disk.
|
|
394
379
|
const canonicalExtensionInputs = toExtensionInputs(config.extensionPacks ?? []);
|
|
395
380
|
const seedResult = await runContractSpaceSeedPhase({
|
|
396
381
|
migrationsDir,
|
|
@@ -463,24 +448,26 @@ async function executeMigrationPlanCommand(
|
|
|
463
448
|
[config.target, config.adapter, ...(config.extensionPacks ?? [])],
|
|
464
449
|
);
|
|
465
450
|
|
|
466
|
-
// Write the planned package's destination
|
|
467
|
-
// resolved target's raw artifacts are written; otherwise
|
|
468
|
-
// `contract.json` / `contract.d.ts` are
|
|
469
|
-
async function
|
|
451
|
+
// Write the planned package's destination contract into the snapshot store.
|
|
452
|
+
// With `--to`, the resolved target's raw artifacts are written; otherwise
|
|
453
|
+
// the emitted `contract.json` / `contract.d.ts` are read from disk.
|
|
454
|
+
async function writeDestinationSnapshot(destHash: string): Promise<void> {
|
|
470
455
|
if (toArtifacts !== null) {
|
|
471
|
-
await
|
|
472
|
-
|
|
473
|
-
toArtifacts.
|
|
474
|
-
|
|
475
|
-
'end-contract',
|
|
476
|
-
);
|
|
456
|
+
await writeContractSnapshot(migrationsDir, destHash, {
|
|
457
|
+
contractJson: toArtifacts.contractJson,
|
|
458
|
+
contractDts: toArtifacts.contractDts,
|
|
459
|
+
});
|
|
477
460
|
return;
|
|
478
461
|
}
|
|
479
462
|
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
480
|
-
await
|
|
481
|
-
|
|
482
|
-
|
|
463
|
+
const [contractJsonRaw, contractDts] = await Promise.all([
|
|
464
|
+
readFile(destinationArtifacts.jsonPath, 'utf-8'),
|
|
465
|
+
readFile(destinationArtifacts.dtsPath, 'utf-8'),
|
|
483
466
|
]);
|
|
467
|
+
await writeContractSnapshot(migrationsDir, destHash, {
|
|
468
|
+
contractJson: JSON.parse(contractJsonRaw) as unknown,
|
|
469
|
+
contractDts,
|
|
470
|
+
});
|
|
484
471
|
}
|
|
485
472
|
|
|
486
473
|
try {
|
|
@@ -507,6 +494,7 @@ async function executeMigrationPlanCommand(
|
|
|
507
494
|
null,
|
|
508
495
|
aggregate.app.spaceId,
|
|
509
496
|
aggregate,
|
|
497
|
+
snapshotsImportPathFrom(baselinePackageDir, migrationsDir),
|
|
510
498
|
);
|
|
511
499
|
if (!baselineLeg.ok) {
|
|
512
500
|
return notOk(baselineLeg.failure);
|
|
@@ -519,12 +507,10 @@ async function executeMigrationPlanCommand(
|
|
|
519
507
|
baselineTimestamp,
|
|
520
508
|
baselineLeg.value,
|
|
521
509
|
);
|
|
522
|
-
await
|
|
523
|
-
|
|
524
|
-
snapshotStartContract.
|
|
525
|
-
|
|
526
|
-
'end-contract',
|
|
527
|
-
);
|
|
510
|
+
await writeContractSnapshot(migrationsDir, fromHash, {
|
|
511
|
+
contractJson: snapshotStartContract.contractJson,
|
|
512
|
+
contractDts: snapshotStartContract.contractDts,
|
|
513
|
+
});
|
|
528
514
|
|
|
529
515
|
if (fromHash === toStorageHash) {
|
|
530
516
|
const baselineOps = baselineLeg.value.hasPlaceholders ? [] : baselineLeg.value.plannedOps;
|
|
@@ -577,6 +563,7 @@ async function executeMigrationPlanCommand(
|
|
|
577
563
|
fromContract,
|
|
578
564
|
aggregate.app.spaceId,
|
|
579
565
|
aggregate,
|
|
566
|
+
snapshotsImportPathFrom(deltaPackageDir, migrationsDir),
|
|
580
567
|
);
|
|
581
568
|
if (!deltaLeg.ok) {
|
|
582
569
|
return notOk(deltaLeg.failure);
|
|
@@ -589,12 +576,11 @@ async function executeMigrationPlanCommand(
|
|
|
589
576
|
deltaTimestamp,
|
|
590
577
|
deltaLeg.value,
|
|
591
578
|
);
|
|
592
|
-
await
|
|
593
|
-
await
|
|
594
|
-
|
|
595
|
-
snapshotStartContract.
|
|
596
|
-
|
|
597
|
-
);
|
|
579
|
+
await writeDestinationSnapshot(toStorageHash);
|
|
580
|
+
await writeContractSnapshot(migrationsDir, fromHash, {
|
|
581
|
+
contractJson: snapshotStartContract.contractJson,
|
|
582
|
+
contractDts: snapshotStartContract.contractDts,
|
|
583
|
+
});
|
|
598
584
|
|
|
599
585
|
const deltaOps = deltaLeg.value.hasPlaceholders ? [] : deltaLeg.value.plannedOps;
|
|
600
586
|
if (deltaLeg.value.hasPlaceholders) {
|
|
@@ -651,6 +637,7 @@ async function executeMigrationPlanCommand(
|
|
|
651
637
|
fromContract,
|
|
652
638
|
aggregate.app.spaceId,
|
|
653
639
|
aggregate,
|
|
640
|
+
snapshotsImportPathFrom(packageDir, migrationsDir),
|
|
654
641
|
);
|
|
655
642
|
if (!deltaLeg.ok) {
|
|
656
643
|
return notOk(deltaLeg.failure);
|
|
@@ -663,21 +650,12 @@ async function executeMigrationPlanCommand(
|
|
|
663
650
|
timestamp,
|
|
664
651
|
deltaLeg.value,
|
|
665
652
|
);
|
|
666
|
-
await
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
{ sourcePath: sourceArtifacts.jsonPath, destName: 'start-contract.json' },
|
|
673
|
-
{ sourcePath: sourceArtifacts.dtsPath, destName: 'start-contract.d.ts' },
|
|
674
|
-
]);
|
|
675
|
-
} else if (snapshotStartContract !== null) {
|
|
676
|
-
await writeSnapshotStartContract(
|
|
677
|
-
packageDir,
|
|
678
|
-
snapshotStartContract.contractJson,
|
|
679
|
-
snapshotStartContract.contractDts,
|
|
680
|
-
);
|
|
653
|
+
await writeDestinationSnapshot(toStorageHash);
|
|
654
|
+
if (snapshotStartContract !== null) {
|
|
655
|
+
await writeContractSnapshot(migrationsDir, snapshotStartContract.fromHash, {
|
|
656
|
+
contractJson: snapshotStartContract.contractJson,
|
|
657
|
+
contractDts: snapshotStartContract.contractDts,
|
|
658
|
+
});
|
|
681
659
|
}
|
|
682
660
|
|
|
683
661
|
if (deltaLeg.value.hasPlaceholders) {
|
package/src/commands/ref.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
1
|
import { loadConfig } from '@prisma-next/config-loader';
|
|
3
2
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
3
|
+
import {
|
|
4
|
+
contractSnapshotDir,
|
|
5
|
+
readContractSnapshotJson,
|
|
6
|
+
} from '@prisma-next/migration-tools/contract-snapshot-store';
|
|
4
7
|
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
5
8
|
import { findLatestMigration, isGraphNode } from '@prisma-next/migration-tools/migration-graph';
|
|
6
9
|
import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
|
|
@@ -114,17 +117,25 @@ export async function executeRefSetCommand(
|
|
|
114
117
|
return notOk(errorRefSetBundleNotFound(resolvedHash));
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
const contractJsonPath = join(
|
|
120
|
+
const contractJsonPath = join(
|
|
121
|
+
contractSnapshotDir(migrationsDir, resolvedHash),
|
|
122
|
+
'contract.json',
|
|
123
|
+
);
|
|
118
124
|
let contractJson: Record<string, unknown>;
|
|
119
125
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
contractJson = (await readContractSnapshotJson(migrationsDir, resolvedHash)) as Record<
|
|
127
|
+
string,
|
|
128
|
+
unknown
|
|
129
|
+
>;
|
|
122
130
|
} catch (readError) {
|
|
123
|
-
if (
|
|
131
|
+
if (
|
|
132
|
+
MigrationToolsError.is(readError) &&
|
|
133
|
+
readError.code === 'MIGRATION.CONTRACT_SNAPSHOT_MISSING'
|
|
134
|
+
) {
|
|
124
135
|
return notOk(
|
|
125
136
|
errorFileNotFound(contractJsonPath, {
|
|
126
|
-
why: `Migration bundle for hash ${resolvedHash} is missing its
|
|
127
|
-
fix: '
|
|
137
|
+
why: `Migration bundle for hash ${resolvedHash} is missing its contract snapshot at ${contractJsonPath}`,
|
|
138
|
+
fix: 'Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.',
|
|
128
139
|
}),
|
|
129
140
|
);
|
|
130
141
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
2
2
|
import { notOk, type Result } from '@prisma-next/utils/result';
|
|
3
|
-
import { join } from 'pathe';
|
|
4
3
|
import {
|
|
5
4
|
CliStructuredError,
|
|
6
5
|
errorContractValidationFailed,
|
|
@@ -66,22 +65,19 @@ export function mapContractAtError(
|
|
|
66
65
|
fix: error.fix,
|
|
67
66
|
}),
|
|
68
67
|
);
|
|
69
|
-
case 'MIGRATION.
|
|
70
|
-
const
|
|
71
|
-
typeof error.details?.['
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
case 'MIGRATION.CONTRACT_SNAPSHOT_MISSING': {
|
|
69
|
+
const expectedPath =
|
|
70
|
+
typeof error.details?.['expectedPath'] === 'string'
|
|
71
|
+
? error.details['expectedPath']
|
|
72
|
+
: 'migrations/snapshots/';
|
|
74
73
|
const role = options?.artifactRole ?? 'from';
|
|
75
74
|
return notOk(
|
|
76
|
-
errorFileNotFound(
|
|
75
|
+
errorFileNotFound(expectedPath, {
|
|
77
76
|
why:
|
|
78
77
|
role === 'to'
|
|
79
|
-
? `Target migration is missing its
|
|
80
|
-
: `Predecessor migration is missing its
|
|
81
|
-
fix:
|
|
82
|
-
role === 'to'
|
|
83
|
-
? 'Re-emit the target migration so its sibling `end-contract.json` / `end-contract.d.ts` are restored, then re-run this command.'
|
|
84
|
-
: 'Re-emit the predecessor migration (`prisma-next migration plan` from its source) so its sibling `end-contract.json` is restored, then re-run this command.',
|
|
78
|
+
? `Target migration is missing its contract snapshot at ${expectedPath}`
|
|
79
|
+
: `Predecessor migration is missing its contract snapshot at ${expectedPath}`,
|
|
80
|
+
fix: 'Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.',
|
|
85
81
|
}),
|
|
86
82
|
);
|
|
87
83
|
}
|