@prisma-next/cli 0.8.0-dev.9 → 0.9.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.mjs +5 -5
- package/dist/{client-XkUw4xD0.mjs → client-Brv4qlfB.mjs} +13 -19
- package/dist/client-Brv4qlfB.mjs.map +1 -0
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +2 -2
- package/dist/commands/db-schema.mjs +1 -1
- package/dist/commands/db-sign.mjs +1 -1
- package/dist/commands/db-update.mjs +2 -2
- package/dist/commands/db-verify.d.mts.map +1 -1
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +1 -1
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +12 -4
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-log.mjs +1 -1
- package/dist/commands/migration-new.mjs +9 -9
- 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-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +7 -7
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +2 -2
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +5 -5
- package/dist/commands/migration-status.mjs.map +1 -1
- package/dist/{contract-emit-GpxW5RLe.mjs → contract-emit-C3STUIBg.mjs} +2 -2
- package/dist/{contract-emit-GpxW5RLe.mjs.map → contract-emit-C3STUIBg.mjs.map} +1 -1
- package/dist/{contract-emit-CgoFk9AU.mjs → contract-emit-iynA3BCA.mjs} +2 -2
- package/dist/{contract-emit-CgoFk9AU.mjs.map → contract-emit-iynA3BCA.mjs.map} +1 -1
- package/dist/{contract-infer-D8edZOCi.mjs → contract-infer-Cnj8G1E2.mjs} +2 -2
- package/dist/{contract-infer-D8edZOCi.mjs.map → contract-infer-Cnj8G1E2.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-D68YpuPR.mjs → contract-space-aggregate-loader-pAc8CDfY.mjs} +2 -2
- package/dist/{contract-space-aggregate-loader-D68YpuPR.mjs.map → contract-space-aggregate-loader-pAc8CDfY.mjs.map} +1 -1
- package/dist/{db-verify-DtRB9iHJ.mjs → db-verify-D7cyH_zz.mjs} +7 -4
- package/dist/db-verify-D7cyH_zz.mjs.map +1 -0
- package/dist/exports/control-api.d.mts +1 -1
- package/dist/exports/control-api.mjs +2 -2
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/index.mjs.map +1 -1
- package/dist/{init-BU2G31T8.mjs → init-Bqg5JWg7.mjs} +3 -3
- package/dist/{init-BU2G31T8.mjs.map → init-Bqg5JWg7.mjs.map} +1 -1
- package/dist/{inspect-live-schema-CPPqCips.mjs → inspect-live-schema-CWLK_lgs.mjs} +2 -2
- package/dist/{inspect-live-schema-CPPqCips.mjs.map → inspect-live-schema-CWLK_lgs.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-B_ezTTwX.mjs → migration-command-scaffold-CmXXC1UZ.mjs} +2 -2
- package/dist/{migration-command-scaffold-B_ezTTwX.mjs.map → migration-command-scaffold-CmXXC1UZ.mjs.map} +1 -1
- package/dist/{migration-plan-DWB-NTxH.mjs → migration-plan-CHyUlBV0.mjs} +34 -27
- package/dist/migration-plan-CHyUlBV0.mjs.map +1 -0
- package/dist/{types-BS_wpjAY.d.mts → types-0aS865QN.d.mts} +13 -7
- package/dist/types-0aS865QN.d.mts.map +1 -0
- package/package.json +17 -17
- package/src/commands/db-verify.ts +19 -3
- package/src/commands/migrate.ts +23 -3
- package/src/commands/migration-new.ts +13 -13
- package/src/commands/migration-plan.ts +50 -31
- package/src/commands/migration-show.ts +9 -5
- package/src/commands/migration-status.ts +5 -5
- package/src/control-api/client.ts +19 -17
- package/src/control-api/operations/contract-emit.ts +13 -2
- package/src/control-api/operations/db-apply-aggregate.ts +1 -1
- package/src/control-api/operations/db-verify.ts +1 -1
- package/src/control-api/operations/migration-apply.ts +1 -1
- package/src/control-api/types.ts +13 -7
- package/src/load-ts-contract.ts +9 -1
- package/src/utils/contract-space-aggregate-loader.ts +2 -2
- package/dist/client-XkUw4xD0.mjs.map +0 -1
- package/dist/db-verify-DtRB9iHJ.mjs.map +0 -1
- package/dist/migration-plan-DWB-NTxH.mjs.map +0 -1
- package/dist/types-BS_wpjAY.d.mts.map +0 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* verbatim.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
12
|
import type { Contract } from '@prisma-next/contract/types';
|
|
13
13
|
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
14
14
|
import { APP_SPACE_ID, createControlStack } from '@prisma-next/framework-components/control';
|
|
@@ -73,11 +73,17 @@ async function executeMigrationNewCommand(
|
|
|
73
73
|
const config = await loadConfig(options.config);
|
|
74
74
|
const { appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(options.config, config);
|
|
75
75
|
|
|
76
|
+
// Construct the family instance up-front so the on-disk contract read
|
|
77
|
+
// below crosses the serializer seam (`familyInstance.deserializeContract`)
|
|
78
|
+
// at the read site, not somewhere downstream. See TML-2536.
|
|
79
|
+
const stack = createControlStack(config);
|
|
80
|
+
const familyInstance = config.family.create(stack);
|
|
81
|
+
|
|
76
82
|
const contractPathAbsolute = resolveContractPath(config);
|
|
77
83
|
|
|
78
84
|
let contractJsonContent: string;
|
|
79
85
|
try {
|
|
80
|
-
contractJsonContent =
|
|
86
|
+
contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
|
|
81
87
|
} catch (error) {
|
|
82
88
|
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
83
89
|
return notOk(
|
|
@@ -90,24 +96,20 @@ async function executeMigrationNewCommand(
|
|
|
90
96
|
throw error;
|
|
91
97
|
}
|
|
92
98
|
|
|
93
|
-
let
|
|
99
|
+
let toContract: Contract;
|
|
94
100
|
try {
|
|
95
|
-
|
|
101
|
+
toContract = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
|
|
96
102
|
} catch (error) {
|
|
97
103
|
return notOk(
|
|
98
104
|
errorRuntime('Contract JSON is invalid', {
|
|
99
|
-
why: `Failed to
|
|
105
|
+
why: `Failed to deserialize ${contractPathAbsolute}: ${error instanceof Error ? error.message : String(error)}`,
|
|
100
106
|
fix: 'Run `prisma-next contract emit` to regenerate the contract',
|
|
101
107
|
}),
|
|
102
108
|
);
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
const toStorageHash =
|
|
106
|
-
|
|
107
|
-
| Record<string, unknown>
|
|
108
|
-
| undefined
|
|
109
|
-
)?.['storageHash'] as string | undefined;
|
|
110
|
-
if (!toStorageHash) {
|
|
111
|
+
const toStorageHash = toContract.storage?.storageHash;
|
|
112
|
+
if (typeof toStorageHash !== 'string') {
|
|
111
113
|
return notOk(
|
|
112
114
|
errorRuntime('Contract is missing storageHash', {
|
|
113
115
|
why: `Contract at ${contractPathAbsolute} has no storageHash`,
|
|
@@ -236,8 +238,6 @@ async function executeMigrationNewCommand(
|
|
|
236
238
|
}
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
const stack = createControlStack(config);
|
|
240
|
-
const familyInstance = config.family.create(stack);
|
|
241
241
|
const planner = migrations.createPlanner(familyInstance);
|
|
242
242
|
const emptyPlan = planner.emptyMigration(
|
|
243
243
|
{
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import type { Contract } from '@prisma-next/contract/types';
|
|
3
3
|
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
4
4
|
import {
|
|
5
|
+
type ControlFamilyInstance,
|
|
5
6
|
createControlStack,
|
|
6
7
|
hasOperationPreview,
|
|
7
8
|
type MigrationPlanOperation,
|
|
@@ -62,16 +63,27 @@ interface MigrationPlanOptions extends CommonCommandOptions {
|
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
65
|
* Load a predecessor migration's destination contract from its sibling
|
|
65
|
-
* `end-contract.json` on disk
|
|
66
|
-
*
|
|
67
|
-
*
|
|
66
|
+
* `end-contract.json` on disk and route it through the family's
|
|
67
|
+
* `ContractSerializer` (via `deserializeContract`) so the in-memory shape
|
|
68
|
+
* is the hydrated `Contract` every other caller sees. Bypassing this
|
|
69
|
+
* seam was the root cause of TML-2536: a raw `JSON.parse(...) as Contract`
|
|
70
|
+
* here let polymorphic `storage.types` entries reach the planner without
|
|
71
|
+
* the `kind` discriminator the planner dispatches on.
|
|
68
72
|
*
|
|
69
|
-
* Throws `CliStructuredError` with
|
|
70
|
-
* sibling file is missing — the user
|
|
71
|
-
* authored the snapshot, and the
|
|
72
|
-
* them at re-emitting from the
|
|
73
|
+
* Throws `CliStructuredError` with:
|
|
74
|
+
* - `errorFileNotFound` when the sibling file is missing — the user
|
|
75
|
+
* has likely deleted or never authored the snapshot, and the
|
|
76
|
+
* message names the file and points them at re-emitting from the
|
|
77
|
+
* source.
|
|
78
|
+
* - `errorContractValidationFailed` when the JSON parses but the
|
|
79
|
+
* family deserializer rejects it (legacy untagged shape, structural
|
|
80
|
+
* mismatch, etc.) — the message names the predecessor's path so
|
|
81
|
+
* the operator can locate the bad snapshot.
|
|
73
82
|
*/
|
|
74
|
-
async function readPredecessorEndContract(
|
|
83
|
+
async function readPredecessorEndContract(
|
|
84
|
+
migrationDir: string,
|
|
85
|
+
familyInstance: ControlFamilyInstance<string, unknown>,
|
|
86
|
+
): Promise<Contract> {
|
|
75
87
|
const path = join(migrationDir, 'end-contract.json');
|
|
76
88
|
let raw: string;
|
|
77
89
|
try {
|
|
@@ -85,7 +97,17 @@ async function readPredecessorEndContract(migrationDir: string): Promise<Contrac
|
|
|
85
97
|
}
|
|
86
98
|
throw error;
|
|
87
99
|
}
|
|
88
|
-
|
|
100
|
+
try {
|
|
101
|
+
return familyInstance.deserializeContract(JSON.parse(raw) as unknown);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (CliStructuredError.is(error)) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
throw errorContractValidationFailed(
|
|
107
|
+
`Predecessor contract at ${path} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
|
|
108
|
+
{ where: { path } },
|
|
109
|
+
);
|
|
110
|
+
}
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
export interface MigrationPlanResult {
|
|
@@ -185,19 +207,26 @@ async function executeMigrationPlanCommand(
|
|
|
185
207
|
);
|
|
186
208
|
}
|
|
187
209
|
|
|
188
|
-
|
|
210
|
+
// Construct the family instance up-front so on-disk reads (the app
|
|
211
|
+
// contract here + every `readPredecessorEndContract` below) cross the
|
|
212
|
+
// serializer seam at the read site, not after the planner has already
|
|
213
|
+
// started dispatching on raw shapes. See TML-2536.
|
|
214
|
+
const stack = createControlStack(config);
|
|
215
|
+
const familyInstance = config.family.create(stack);
|
|
216
|
+
|
|
217
|
+
let toContract: Contract;
|
|
189
218
|
try {
|
|
190
|
-
|
|
219
|
+
toContract = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
|
|
191
220
|
} catch (error) {
|
|
192
221
|
return notOk(
|
|
193
222
|
errorContractValidationFailed(
|
|
194
|
-
`Contract
|
|
223
|
+
`Contract at ${contractPathAbsolute} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
|
|
195
224
|
{ where: { path: contractPathAbsolute } },
|
|
196
225
|
),
|
|
197
226
|
);
|
|
198
227
|
}
|
|
199
228
|
|
|
200
|
-
const rawStorageHash =
|
|
229
|
+
const rawStorageHash = toContract.storage?.storageHash;
|
|
201
230
|
if (typeof rawStorageHash !== 'string') {
|
|
202
231
|
return notOk(
|
|
203
232
|
errorContractValidationFailed('Contract is missing storageHash', {
|
|
@@ -235,7 +264,7 @@ async function executeMigrationPlanCommand(
|
|
|
235
264
|
);
|
|
236
265
|
}
|
|
237
266
|
fromContractSourceDir = matchingBundle.dirPath;
|
|
238
|
-
fromContract = await readPredecessorEndContract(fromContractSourceDir);
|
|
267
|
+
fromContract = await readPredecessorEndContract(fromContractSourceDir, familyInstance);
|
|
239
268
|
} else {
|
|
240
269
|
const latestMigration = findLatestMigration(graph);
|
|
241
270
|
if (latestMigration) {
|
|
@@ -245,7 +274,7 @@ async function executeMigrationPlanCommand(
|
|
|
245
274
|
);
|
|
246
275
|
if (leafPkg) {
|
|
247
276
|
fromContractSourceDir = leafPkg.dirPath;
|
|
248
|
-
fromContract = await readPredecessorEndContract(fromContractSourceDir);
|
|
277
|
+
fromContract = await readPredecessorEndContract(fromContractSourceDir, familyInstance);
|
|
249
278
|
}
|
|
250
279
|
}
|
|
251
280
|
}
|
|
@@ -324,26 +353,16 @@ async function executeMigrationPlanCommand(
|
|
|
324
353
|
// Phase 2 — load: build the aggregate against the now-consistent disk
|
|
325
354
|
// state that phase 1 just seeded. The seed phase guarantees every
|
|
326
355
|
// declared extension has its head ref pinned, so the loader's
|
|
327
|
-
// declaredButUnmigrated precheck always passes here.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
try {
|
|
332
|
-
validatedAppContract = familyInstance.validateContract(toContractJson);
|
|
333
|
-
} catch (error) {
|
|
334
|
-
return notOk(
|
|
335
|
-
errorContractValidationFailed(
|
|
336
|
-
`Contract validation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
337
|
-
{ where: { path: contractPathAbsolute } },
|
|
338
|
-
),
|
|
339
|
-
);
|
|
340
|
-
}
|
|
356
|
+
// declaredButUnmigrated precheck always passes here. The app contract
|
|
357
|
+
// was already routed through `familyInstance.deserializeContract` at the
|
|
358
|
+
// read site above (see TML-2536), so it's the hydrated `Contract`
|
|
359
|
+
// here — no second validation pass needed.
|
|
341
360
|
const aggregateResult = await buildContractSpaceAggregate({
|
|
342
361
|
targetId: config.target.targetId,
|
|
343
362
|
migrationsDir,
|
|
344
|
-
appContract:
|
|
363
|
+
appContract: toContract,
|
|
345
364
|
extensionPacks: config.extensionPacks ?? [],
|
|
346
|
-
|
|
365
|
+
deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
|
|
347
366
|
});
|
|
348
367
|
if (!aggregateResult.ok) {
|
|
349
368
|
return notOk(aggregateResult.failure);
|
|
@@ -370,26 +370,30 @@ async function executeMigrationShowCommand(
|
|
|
370
370
|
);
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
// Construct the family instance up-front so the on-disk app contract
|
|
374
|
+
// read crosses the serializer seam (`familyInstance.deserializeContract`)
|
|
375
|
+
// at the read site. See TML-2536.
|
|
376
|
+
const stack = createControlStack(config);
|
|
377
|
+
const familyInstance = config.family.create(stack);
|
|
378
|
+
|
|
373
379
|
let appContract: Contract;
|
|
374
380
|
try {
|
|
375
|
-
appContract = JSON.parse(contractJsonContent) as
|
|
381
|
+
appContract = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
|
|
376
382
|
} catch (error) {
|
|
377
383
|
return notOk(
|
|
378
384
|
errorContractValidationFailed(
|
|
379
|
-
`Contract
|
|
385
|
+
`Contract at ${contractPathAbsolute} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
|
|
380
386
|
{ where: { path: contractPathAbsolute } },
|
|
381
387
|
),
|
|
382
388
|
);
|
|
383
389
|
}
|
|
384
390
|
|
|
385
|
-
const stack = createControlStack(config);
|
|
386
|
-
const familyInstance = config.family.create(stack);
|
|
387
391
|
const aggregateResult = await buildContractSpaceAggregate({
|
|
388
392
|
targetId: config.target.targetId,
|
|
389
393
|
migrationsDir,
|
|
390
394
|
appContract,
|
|
391
395
|
extensionPacks: config.extensionPacks ?? [],
|
|
392
|
-
|
|
396
|
+
deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
|
|
393
397
|
});
|
|
394
398
|
if (!aggregateResult.ok) {
|
|
395
399
|
return notOk(aggregateResult.failure);
|
|
@@ -437,15 +437,15 @@ export async function loadAggregateStatusSpaces(args: {
|
|
|
437
437
|
readonly migrationsDir: string;
|
|
438
438
|
readonly appContractRaw: unknown;
|
|
439
439
|
readonly extensionPacks: BuildAggregateInputs<string, string>['extensionPacks'];
|
|
440
|
-
readonly
|
|
440
|
+
readonly deserializeContract: BuildAggregateInputs<string, string>['deserializeContract'];
|
|
441
441
|
readonly markersBySpace: ReadonlyMap<string, ContractMarkerRecordLike> | null;
|
|
442
442
|
}): Promise<readonly MigrationStatusSpaceEntry[]> {
|
|
443
443
|
const loadInputs: BuildAggregateInputs<string, string> = {
|
|
444
444
|
targetId: args.targetId,
|
|
445
445
|
migrationsDir: args.migrationsDir,
|
|
446
|
-
appContract: args.
|
|
446
|
+
appContract: args.deserializeContract(args.appContractRaw),
|
|
447
447
|
extensionPacks: args.extensionPacks,
|
|
448
|
-
|
|
448
|
+
deserializeContract: args.deserializeContract,
|
|
449
449
|
};
|
|
450
450
|
|
|
451
451
|
const loaded = await buildContractSpaceAggregate(loadInputs);
|
|
@@ -774,7 +774,7 @@ async function executeMigrationStatusCommand(
|
|
|
774
774
|
let aggregateSpaces: readonly MigrationStatusSpaceEntry[] = [];
|
|
775
775
|
if (contractRawForAggregate !== null) {
|
|
776
776
|
// The aggregate loader needs a typed-Contract producer. Build a
|
|
777
|
-
// real control stack so `
|
|
777
|
+
// real control stack so `deserializeContract` runs against a fully
|
|
778
778
|
// composed family instance — descriptors that read stack members
|
|
779
779
|
// during construction (e.g. codec lookups) get a consistent view.
|
|
780
780
|
const stack = createControlStack(config);
|
|
@@ -785,7 +785,7 @@ async function executeMigrationStatusCommand(
|
|
|
785
785
|
migrationsDir,
|
|
786
786
|
appContractRaw: contractRawForAggregate,
|
|
787
787
|
extensionPacks: config.extensionPacks ?? [],
|
|
788
|
-
|
|
788
|
+
deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
|
|
789
789
|
markersBySpace: allMarkers,
|
|
790
790
|
});
|
|
791
791
|
} catch {
|
|
@@ -201,7 +201,7 @@ class ControlClientImpl implements ControlClient {
|
|
|
201
201
|
// Validate contract using family instance
|
|
202
202
|
let contract: Contract;
|
|
203
203
|
try {
|
|
204
|
-
contract = familyInstance.
|
|
204
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
205
205
|
} catch (error) {
|
|
206
206
|
const message = error instanceof Error ? error.message : String(error);
|
|
207
207
|
throw new ContractValidationError(message, error);
|
|
@@ -254,7 +254,7 @@ class ControlClientImpl implements ControlClient {
|
|
|
254
254
|
// Validate contract using family instance
|
|
255
255
|
let contract: Contract;
|
|
256
256
|
try {
|
|
257
|
-
contract = familyInstance.
|
|
257
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
258
258
|
} catch (error) {
|
|
259
259
|
const message = error instanceof Error ? error.message : String(error);
|
|
260
260
|
throw new ContractValidationError(message, error);
|
|
@@ -308,7 +308,7 @@ class ControlClientImpl implements ControlClient {
|
|
|
308
308
|
// Validate contract using family instance
|
|
309
309
|
let contract: Contract;
|
|
310
310
|
try {
|
|
311
|
-
contract = familyInstance.
|
|
311
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
312
312
|
} catch (error) {
|
|
313
313
|
const message = error instanceof Error ? error.message : String(error);
|
|
314
314
|
throw new ContractValidationError(message, error);
|
|
@@ -361,7 +361,7 @@ class ControlClientImpl implements ControlClient {
|
|
|
361
361
|
|
|
362
362
|
let contract: Contract;
|
|
363
363
|
try {
|
|
364
|
-
contract = familyInstance.
|
|
364
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
365
365
|
} catch (error) {
|
|
366
366
|
const message = error instanceof Error ? error.message : String(error);
|
|
367
367
|
throw new ContractValidationError(message, error);
|
|
@@ -392,7 +392,7 @@ class ControlClientImpl implements ControlClient {
|
|
|
392
392
|
|
|
393
393
|
let contract: Contract;
|
|
394
394
|
try {
|
|
395
|
-
contract = familyInstance.
|
|
395
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
396
396
|
} catch (error) {
|
|
397
397
|
const message = error instanceof Error ? error.message : String(error);
|
|
398
398
|
throw new ContractValidationError(message, error);
|
|
@@ -418,18 +418,10 @@ class ControlClientImpl implements ControlClient {
|
|
|
418
418
|
await this.connectWithProgress(options.connection, 'dbVerify', onProgress);
|
|
419
419
|
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
420
420
|
|
|
421
|
-
let contract: Contract;
|
|
422
|
-
try {
|
|
423
|
-
contract = familyInstance.validateContract(options.contract);
|
|
424
|
-
} catch (error) {
|
|
425
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
426
|
-
throw new ContractValidationError(message, error);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
421
|
return executeDbVerify({
|
|
430
422
|
driver,
|
|
431
423
|
familyInstance,
|
|
432
|
-
contract,
|
|
424
|
+
contract: options.contract,
|
|
433
425
|
migrationsDir: options.migrationsDir,
|
|
434
426
|
targetId: this.options.target.targetId,
|
|
435
427
|
extensionPacks: this.options.extensionPacks ?? [],
|
|
@@ -466,7 +458,7 @@ class ControlClientImpl implements ControlClient {
|
|
|
466
458
|
|
|
467
459
|
let contract: Contract;
|
|
468
460
|
try {
|
|
469
|
-
contract = familyInstance.
|
|
461
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
470
462
|
} catch (error) {
|
|
471
463
|
const message = error instanceof Error ? error.message : String(error);
|
|
472
464
|
throw new ContractValidationError(message, error);
|
|
@@ -641,10 +633,20 @@ class ControlClientImpl implements ControlClient {
|
|
|
641
633
|
});
|
|
642
634
|
|
|
643
635
|
try {
|
|
644
|
-
|
|
636
|
+
// Blind cast: `contractRaw` is the unverified provider
|
|
637
|
+
// payload — `enrichContract` only adds capability + extension
|
|
638
|
+
// metadata onto whatever shape it receives. The structural
|
|
639
|
+
// check happens immediately afterwards via
|
|
640
|
+
// `familyInstance.deserializeContract(enrichedIR)`, which is
|
|
641
|
+
// the seam-of-record and the only thing that may surface
|
|
642
|
+
// structural errors to the caller.
|
|
643
|
+
const enrichedIR = enrichContract(
|
|
644
|
+
contractRaw as unknown as Contract,
|
|
645
|
+
this.frameworkComponents ?? [],
|
|
646
|
+
);
|
|
645
647
|
|
|
646
648
|
try {
|
|
647
|
-
this.familyInstance.
|
|
649
|
+
this.familyInstance.deserializeContract(enrichedIR);
|
|
648
650
|
} catch (error) {
|
|
649
651
|
onProgress?.({
|
|
650
652
|
action: 'emit',
|
|
@@ -232,8 +232,19 @@ export async function executeContractEmit(
|
|
|
232
232
|
config.target.targetId,
|
|
233
233
|
rawComponents,
|
|
234
234
|
);
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
// Blind cast: `validateProviderResult` upstream has already
|
|
236
|
+
// pinned `validatedContract.value` to the provider's loose
|
|
237
|
+
// `Contract` envelope, but the local `Contract` type at this
|
|
238
|
+
// call site is the precise structural interface. Re-narrowing
|
|
239
|
+
// the envelope into the precise type is exactly what the
|
|
240
|
+
// `familyInstance.deserializeContract(enrichedIR)` call on the
|
|
241
|
+
// next line does — the cast just defers the structural check
|
|
242
|
+
// by one statement so `enrichContract` can decorate first.
|
|
243
|
+
const enrichedIR = enrichContract(
|
|
244
|
+
validatedContract.value as unknown as Contract,
|
|
245
|
+
frameworkComponents,
|
|
246
|
+
);
|
|
247
|
+
familyInstance.deserializeContract(enrichedIR);
|
|
237
248
|
// Each target's descriptor ships a `contractSerializer` SPI; the
|
|
238
249
|
// framework canonicalizer threads its `serializeContract` so the
|
|
239
250
|
// on-disk JSON envelope is constructed by target-owned code
|
|
@@ -116,7 +116,7 @@ export async function executeAggregateApply<TFamilyId extends string, TTargetId
|
|
|
116
116
|
migrationsDir,
|
|
117
117
|
appContract: contract,
|
|
118
118
|
extensionPacks,
|
|
119
|
-
|
|
119
|
+
deserializeContract: (json) => familyInstance.deserializeContract(json),
|
|
120
120
|
};
|
|
121
121
|
const loaded = await buildContractSpaceAggregate(loadInputs);
|
|
122
122
|
if (!loaded.ok) {
|
|
@@ -120,7 +120,7 @@ function buildLoadInputs<TFamilyId extends string, TTargetId extends string>(
|
|
|
120
120
|
migrationsDir: options.migrationsDir,
|
|
121
121
|
appContract: options.contract,
|
|
122
122
|
extensionPacks: options.extensionPacks,
|
|
123
|
-
|
|
123
|
+
deserializeContract: (json) => options.familyInstance.deserializeContract(json),
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -137,7 +137,7 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
|
|
|
137
137
|
migrationsDir,
|
|
138
138
|
appContract: contract,
|
|
139
139
|
extensionPacks,
|
|
140
|
-
|
|
140
|
+
deserializeContract: (json) => familyInstance.deserializeContract(json),
|
|
141
141
|
appMigrationPackages,
|
|
142
142
|
};
|
|
143
143
|
const loaded = await buildContractSpaceAggregate(loadInputs);
|
package/src/control-api/types.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
ContractSourceDiagnostics,
|
|
3
3
|
ContractSourceProvider,
|
|
4
4
|
} from '@prisma-next/config/config-types';
|
|
5
|
-
import type { ContractMarkerRecord } from '@prisma-next/contract/types';
|
|
5
|
+
import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
|
|
6
6
|
import type {
|
|
7
7
|
ControlAdapterDescriptor,
|
|
8
8
|
ControlDriverDescriptor,
|
|
@@ -118,7 +118,7 @@ export type OnControlProgress = (event: ControlProgressEvent) => void;
|
|
|
118
118
|
* Options for the verify operation.
|
|
119
119
|
*/
|
|
120
120
|
export interface VerifyOptions {
|
|
121
|
-
/** Contract or unvalidated JSON - validated at runtime via familyInstance.
|
|
121
|
+
/** Contract or unvalidated JSON - validated at runtime via familyInstance.deserializeContract() */
|
|
122
122
|
readonly contract: unknown;
|
|
123
123
|
/**
|
|
124
124
|
* Database connection. If provided, verify will connect before executing.
|
|
@@ -134,7 +134,7 @@ export interface VerifyOptions {
|
|
|
134
134
|
* Options for the schemaVerify operation.
|
|
135
135
|
*/
|
|
136
136
|
export interface SchemaVerifyOptions {
|
|
137
|
-
/** Contract or unvalidated JSON - validated at runtime via familyInstance.
|
|
137
|
+
/** Contract or unvalidated JSON - validated at runtime via familyInstance.deserializeContract() */
|
|
138
138
|
readonly contract: unknown;
|
|
139
139
|
/**
|
|
140
140
|
* Whether to use strict mode for schema verification.
|
|
@@ -156,7 +156,7 @@ export interface SchemaVerifyOptions {
|
|
|
156
156
|
* Options for the sign operation.
|
|
157
157
|
*/
|
|
158
158
|
export interface SignOptions {
|
|
159
|
-
/** Contract or unvalidated JSON - validated at runtime via familyInstance.
|
|
159
|
+
/** Contract or unvalidated JSON - validated at runtime via familyInstance.deserializeContract() */
|
|
160
160
|
readonly contract: unknown;
|
|
161
161
|
/**
|
|
162
162
|
* Path to the contract file (for metadata in the result).
|
|
@@ -180,7 +180,7 @@ export interface SignOptions {
|
|
|
180
180
|
* Options for the dbInit operation.
|
|
181
181
|
*/
|
|
182
182
|
export interface DbInitOptions {
|
|
183
|
-
/** Contract or unvalidated JSON - validated at runtime via familyInstance.
|
|
183
|
+
/** Contract or unvalidated JSON - validated at runtime via familyInstance.deserializeContract() */
|
|
184
184
|
readonly contract: unknown;
|
|
185
185
|
/**
|
|
186
186
|
* Mode for the dbInit operation.
|
|
@@ -209,7 +209,7 @@ export interface DbInitOptions {
|
|
|
209
209
|
* Options for the dbUpdate operation.
|
|
210
210
|
*/
|
|
211
211
|
export interface DbUpdateOptions {
|
|
212
|
-
/** Contract or unvalidated JSON - validated at runtime via familyInstance.
|
|
212
|
+
/** Contract or unvalidated JSON - validated at runtime via familyInstance.deserializeContract() */
|
|
213
213
|
readonly contract: unknown;
|
|
214
214
|
/**
|
|
215
215
|
* Mode for the dbUpdate operation.
|
|
@@ -251,7 +251,13 @@ export interface DbUpdateOptions {
|
|
|
251
251
|
* portion of the verifier.
|
|
252
252
|
*/
|
|
253
253
|
export interface DbVerifyOptions {
|
|
254
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Already-deserialized contract. Callers cross the family
|
|
256
|
+
* `deserializeContract` seam at the read site (TML-2536) and pass the
|
|
257
|
+
* hydrated value through unchanged; this op no longer re-runs the
|
|
258
|
+
* SerializerBase pipeline.
|
|
259
|
+
*/
|
|
260
|
+
readonly contract: Contract;
|
|
255
261
|
readonly migrationsDir: string;
|
|
256
262
|
readonly strict: boolean;
|
|
257
263
|
readonly skipSchema: boolean;
|
package/src/load-ts-contract.ts
CHANGED
|
@@ -212,7 +212,15 @@ export async function loadContractFromTs(
|
|
|
212
212
|
|
|
213
213
|
validatePurity(contract);
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
// Blind cast: the loaded module was authored by user code
|
|
216
|
+
// (typically via `defineContract` / a contract builder) and
|
|
217
|
+
// its runtime shape is structurally a `Contract`, but the
|
|
218
|
+
// dynamic import collapses the source typing. The contract
|
|
219
|
+
// structural validation that asserts the shape happens
|
|
220
|
+
// downstream at the `familyInstance.deserializeContract` seam
|
|
221
|
+
// (e.g. in `executeContractEmit`); this helper only checks
|
|
222
|
+
// purity here.
|
|
223
|
+
return contract as unknown as Contract;
|
|
216
224
|
} catch (error) {
|
|
217
225
|
try {
|
|
218
226
|
if (tempFile) {
|
|
@@ -120,7 +120,7 @@ export interface BuildAggregateInputs<TFamilyId extends string, TTargetId extend
|
|
|
120
120
|
readonly migrationsDir: string;
|
|
121
121
|
readonly appContract: Contract;
|
|
122
122
|
readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
|
|
123
|
-
readonly
|
|
123
|
+
readonly deserializeContract: (contractJson: unknown) => Contract;
|
|
124
124
|
/**
|
|
125
125
|
* App-space migration packages to hydrate the app member's
|
|
126
126
|
* migration graph with. Defaults to `[]` (matches the `db init` /
|
|
@@ -165,7 +165,7 @@ export async function buildContractSpaceAggregate<
|
|
|
165
165
|
migrationsDir: inputs.migrationsDir,
|
|
166
166
|
appContract: inputs.appContract,
|
|
167
167
|
declaredExtensions,
|
|
168
|
-
|
|
168
|
+
deserializeContract: inputs.deserializeContract,
|
|
169
169
|
appMigrationPackages: inputs.appMigrationPackages ?? [],
|
|
170
170
|
};
|
|
171
171
|
|