@prisma-next/family-sql 0.12.0-dev.6 → 0.12.0-dev.61
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/dist/{authoring-type-constructors-F4JpCJl7.mjs → authoring-type-constructors-D4lQ-qpj.mjs} +1 -1
- package/dist/{authoring-type-constructors-F4JpCJl7.mjs.map → authoring-type-constructors-D4lQ-qpj.mjs.map} +1 -1
- package/dist/control-adapter-76pzcIFV.d.mts +172 -0
- package/dist/control-adapter-76pzcIFV.d.mts.map +1 -0
- package/dist/control-adapter.d.mts +2 -109
- package/dist/control.d.mts +118 -4
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +305 -44
- package/dist/control.mjs.map +1 -1
- package/dist/ir.d.mts +4 -5
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +1 -1
- package/dist/migration.d.mts +1 -1
- package/dist/migration.d.mts.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/runtime.d.mts +4 -2
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +4 -2
- package/dist/runtime.mjs.map +1 -1
- package/dist/schema-verify.d.mts +2 -1
- package/dist/schema-verify.d.mts.map +1 -1
- package/dist/schema-verify.mjs +1 -1
- package/dist/{sql-contract-serializer-8axtK4lg.mjs → sql-contract-serializer-CY7qnms7.mjs} +18 -36
- package/dist/sql-contract-serializer-CY7qnms7.mjs.map +1 -0
- package/dist/{timestamp-now-generator-BkjCQIde.mjs → timestamp-now-generator-CloimujU.mjs} +1 -1
- package/dist/{timestamp-now-generator-BkjCQIde.mjs.map → timestamp-now-generator-CloimujU.mjs.map} +1 -1
- package/dist/{types-CeeCStqw.d.mts → types-CRpx_fk5.d.mts} +69 -15
- package/dist/types-CRpx_fk5.d.mts.map +1 -0
- package/dist/{verify-Crewz6hG.mjs → verify-C-G0obRm.mjs} +1 -1
- package/dist/{verify-Crewz6hG.mjs.map → verify-C-G0obRm.mjs.map} +1 -1
- package/dist/{verify-sql-schema-CN7pPoTC.d.mts → verify-sql-schema-DcMaT5Zj.d.mts} +1 -1
- package/dist/{verify-sql-schema-CN7pPoTC.d.mts.map → verify-sql-schema-DcMaT5Zj.d.mts.map} +1 -1
- package/dist/{verify-sql-schema-CYLsGCFO.mjs → verify-sql-schema-d3P9NA_8.mjs} +400 -317
- package/dist/verify-sql-schema-d3P9NA_8.mjs.map +1 -0
- package/dist/verify.mjs +1 -1
- package/package.json +23 -23
- package/src/core/control-adapter.ts +105 -7
- package/src/core/control-instance.ts +260 -66
- package/src/core/default-namespace.ts +9 -0
- package/src/core/ir/sql-contract-serializer-base.ts +72 -56
- package/src/core/migrations/contract-to-schema-ir.ts +13 -9
- package/src/core/migrations/control-policy.ts +322 -0
- package/src/core/migrations/field-event-planner.ts +2 -2
- package/src/core/migrations/plan-helpers.ts +16 -0
- package/src/core/migrations/types.ts +16 -6
- package/src/core/schema-verify/control-verify-emit.ts +46 -0
- package/src/core/schema-verify/verifier-disposition.ts +53 -0
- package/src/core/schema-verify/verify-helpers.ts +151 -110
- package/src/core/schema-verify/verify-sql-schema.ts +281 -178
- package/src/exports/control.ts +6 -0
- package/src/exports/runtime.ts +7 -0
- package/dist/control-adapter.d.mts.map +0 -1
- package/dist/sql-contract-serializer-8axtK4lg.mjs.map +0 -1
- package/dist/types-CeeCStqw.d.mts.map +0 -1
- package/dist/verify-sql-schema-CYLsGCFO.mjs.map +0 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Contract,
|
|
3
|
+
ContractMarkerRecord,
|
|
4
|
+
LedgerEntryRecord,
|
|
5
|
+
} from '@prisma-next/contract/types';
|
|
2
6
|
import type {
|
|
3
7
|
TargetBoundComponentDescriptor,
|
|
4
8
|
TargetDescriptor,
|
|
5
9
|
} from '@prisma-next/framework-components/components';
|
|
6
10
|
import type {
|
|
7
|
-
ControlDriverInstance,
|
|
8
11
|
ControlFamilyInstance,
|
|
9
12
|
ControlStack,
|
|
10
13
|
CoreSchemaView,
|
|
@@ -28,17 +31,13 @@ import type { TypesImportSpec } from '@prisma-next/framework-components/emission
|
|
|
28
31
|
import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
|
|
29
32
|
import { assertDescriptorSelfConsistency } from '@prisma-next/migration-tools/spaces';
|
|
30
33
|
import { sqlContractCanonicalizationHooks } from '@prisma-next/sql-contract/canonicalization-hooks';
|
|
31
|
-
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
34
|
+
import type { SqlControlDriverInstance, SqlStorage } from '@prisma-next/sql-contract/types';
|
|
32
35
|
import type {
|
|
33
36
|
AnyQueryAst,
|
|
37
|
+
DdlNode,
|
|
34
38
|
LoweredStatement,
|
|
35
39
|
LowererContext,
|
|
36
40
|
} from '@prisma-next/sql-relational-core/ast';
|
|
37
|
-
import {
|
|
38
|
-
ensureSchemaStatement,
|
|
39
|
-
ensureTableStatement,
|
|
40
|
-
writeContractMarker,
|
|
41
|
-
} from '@prisma-next/sql-runtime';
|
|
42
41
|
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
43
42
|
import type { SqlSchemaIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
|
|
44
43
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
@@ -69,10 +68,10 @@ function extractCodecTypeIdsFromContract(contract: unknown): readonly string[] {
|
|
|
69
68
|
) {
|
|
70
69
|
const namespaces = contract.storage.namespaces as Record<
|
|
71
70
|
string,
|
|
72
|
-
{ readonly
|
|
71
|
+
{ readonly entries: { readonly table: Readonly<Record<string, unknown>> } }
|
|
73
72
|
>;
|
|
74
73
|
for (const ns of Object.values(namespaces)) {
|
|
75
|
-
const tbls = ns.
|
|
74
|
+
const tbls = ns.entries.table;
|
|
76
75
|
if (typeof tbls !== 'object' || tbls === null) continue;
|
|
77
76
|
for (const table of Object.values(tbls)) {
|
|
78
77
|
if (
|
|
@@ -203,7 +202,7 @@ export interface SqlControlFamilyInstance
|
|
|
203
202
|
deserializeContract(contractJson: unknown): Contract;
|
|
204
203
|
|
|
205
204
|
verify(options: {
|
|
206
|
-
readonly driver:
|
|
205
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
207
206
|
readonly contract: unknown;
|
|
208
207
|
readonly expectedTargetId: string;
|
|
209
208
|
readonly contractPath: string;
|
|
@@ -228,43 +227,77 @@ export interface SqlControlFamilyInstance
|
|
|
228
227
|
}): VerifyDatabaseSchemaResult;
|
|
229
228
|
|
|
230
229
|
sign(options: {
|
|
231
|
-
readonly driver:
|
|
230
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
232
231
|
readonly contract: unknown;
|
|
233
232
|
readonly contractPath: string;
|
|
234
233
|
readonly configPath?: string;
|
|
235
234
|
}): Promise<SignDatabaseResult>;
|
|
236
235
|
|
|
237
236
|
introspect(options: {
|
|
238
|
-
readonly driver:
|
|
237
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
239
238
|
readonly contract?: unknown;
|
|
240
239
|
}): Promise<SqlSchemaIR>;
|
|
241
240
|
|
|
242
241
|
inferPslContract(schemaIR: SqlSchemaIR): PslDocumentAst;
|
|
243
242
|
|
|
244
|
-
lowerAst(ast: AnyQueryAst, context: LowererContext<unknown>): LoweredStatement;
|
|
243
|
+
lowerAst(ast: AnyQueryAst | DdlNode, context: LowererContext<unknown>): LoweredStatement;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Inserts the initial marker row for `space` (upsert on `space`).
|
|
247
|
+
* Delegates to the target control adapter's write SPI; see
|
|
248
|
+
* `SqlControlAdapter.initMarker`.
|
|
249
|
+
*/
|
|
250
|
+
initMarker(options: {
|
|
251
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
252
|
+
readonly space: string;
|
|
253
|
+
readonly destination: {
|
|
254
|
+
readonly storageHash: string;
|
|
255
|
+
readonly profileHash: string;
|
|
256
|
+
readonly invariants?: readonly string[];
|
|
257
|
+
};
|
|
258
|
+
}): Promise<void>;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Compare-and-swap advance of the marker row for `space`. Returns `true`
|
|
262
|
+
* when the swap matched a row; see `SqlControlAdapter.updateMarker`.
|
|
263
|
+
*/
|
|
264
|
+
updateMarker(options: {
|
|
265
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
266
|
+
readonly space: string;
|
|
267
|
+
readonly expectedFrom: string;
|
|
268
|
+
readonly destination: {
|
|
269
|
+
readonly storageHash: string;
|
|
270
|
+
readonly profileHash: string;
|
|
271
|
+
readonly invariants?: readonly string[];
|
|
272
|
+
};
|
|
273
|
+
}): Promise<boolean>;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Appends a ledger entry for `space`; see
|
|
277
|
+
* `SqlControlAdapter.writeLedgerEntry`.
|
|
278
|
+
*/
|
|
279
|
+
writeLedgerEntry(options: {
|
|
280
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
281
|
+
readonly space: string;
|
|
282
|
+
readonly entry: {
|
|
283
|
+
readonly edgeId: string;
|
|
284
|
+
readonly from: string;
|
|
285
|
+
readonly to: string;
|
|
286
|
+
readonly migrationName: string;
|
|
287
|
+
readonly migrationHash: string;
|
|
288
|
+
readonly operations: readonly unknown[];
|
|
289
|
+
};
|
|
290
|
+
}): Promise<void>;
|
|
291
|
+
|
|
292
|
+
bootstrapControlTableQueries(): readonly DdlNode[];
|
|
293
|
+
|
|
294
|
+
bootstrapSignMarkerQueries(): readonly DdlNode[];
|
|
245
295
|
|
|
246
296
|
toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview;
|
|
247
297
|
}
|
|
248
298
|
|
|
249
299
|
export type SqlFamilyInstance = SqlControlFamilyInstance;
|
|
250
300
|
|
|
251
|
-
function isSqlControlAdapter<TTargetId extends string>(
|
|
252
|
-
value: unknown,
|
|
253
|
-
): value is SqlControlAdapter<TTargetId> {
|
|
254
|
-
return (
|
|
255
|
-
typeof value === 'object' &&
|
|
256
|
-
value !== null &&
|
|
257
|
-
'introspect' in value &&
|
|
258
|
-
typeof (value as { introspect: unknown }).introspect === 'function' &&
|
|
259
|
-
'readMarker' in value &&
|
|
260
|
-
typeof (value as { readMarker: unknown }).readMarker === 'function' &&
|
|
261
|
-
'readAllMarkers' in value &&
|
|
262
|
-
typeof (value as { readAllMarkers: unknown }).readAllMarkers === 'function' &&
|
|
263
|
-
'lower' in value &&
|
|
264
|
-
typeof (value as { lower: unknown }).lower === 'function'
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
301
|
interface DescriptorWithStorageTypes {
|
|
269
302
|
readonly targetId?: string | undefined;
|
|
270
303
|
readonly types?:
|
|
@@ -314,6 +347,113 @@ function buildSqlTypeMetadataRegistry(options: {
|
|
|
314
347
|
return registry;
|
|
315
348
|
}
|
|
316
349
|
|
|
350
|
+
interface CrossSpaceFkView {
|
|
351
|
+
readonly id: string;
|
|
352
|
+
readonly contractSpace?: {
|
|
353
|
+
readonly contractJson?: {
|
|
354
|
+
readonly extensionPacks?: Readonly<Record<string, unknown>>;
|
|
355
|
+
readonly storage?: {
|
|
356
|
+
readonly namespaces?: Readonly<Record<string, unknown>>;
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Builds a map from each extension id to the set of extension ids it
|
|
364
|
+
* transitively depends on. Uses the same declared-dependency data that
|
|
365
|
+
* `buildExtensionLoadOrder` in control-stack uses.
|
|
366
|
+
*/
|
|
367
|
+
function buildTransitiveDependsOnMap(
|
|
368
|
+
extensions: readonly CrossSpaceFkView[],
|
|
369
|
+
): Map<string, Set<string>> {
|
|
370
|
+
const directDeps = new Map<string, readonly string[]>();
|
|
371
|
+
for (const ext of extensions) {
|
|
372
|
+
const packs = ext.contractSpace?.contractJson?.extensionPacks;
|
|
373
|
+
const deps = packs !== null && typeof packs === 'object' ? Object.keys(packs) : [];
|
|
374
|
+
directDeps.set(ext.id, deps);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const result = new Map<string, Set<string>>();
|
|
378
|
+
const resolve = (id: string, visiting: Set<string>): Set<string> => {
|
|
379
|
+
const cached = result.get(id);
|
|
380
|
+
if (cached !== undefined) return cached;
|
|
381
|
+
const set = new Set<string>();
|
|
382
|
+
result.set(id, set);
|
|
383
|
+
for (const depId of directDeps.get(id) ?? []) {
|
|
384
|
+
set.add(depId);
|
|
385
|
+
if (!visiting.has(depId)) {
|
|
386
|
+
visiting.add(depId);
|
|
387
|
+
for (const transitive of resolve(depId, visiting)) {
|
|
388
|
+
set.add(transitive);
|
|
389
|
+
}
|
|
390
|
+
visiting.delete(depId);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return set;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
for (const ext of extensions) {
|
|
397
|
+
resolve(ext.id, new Set([ext.id]));
|
|
398
|
+
}
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Asserts that no cross-space FK in any extension points against the
|
|
404
|
+
* dependency direction.
|
|
405
|
+
*
|
|
406
|
+
* A cross-space FK (target.spaceId present) from extension A pointing at
|
|
407
|
+
* space B is a violation when B depends on A (directly or transitively),
|
|
408
|
+
* because that means A is pointing "upward" against the dependency arrows
|
|
409
|
+
* established by the extension load order.
|
|
410
|
+
*
|
|
411
|
+
* Throws with a diagnostic naming the violating extension (source), the
|
|
412
|
+
* target space, and the direction violation.
|
|
413
|
+
*/
|
|
414
|
+
function isObjectRecord(v: unknown): v is Record<string, unknown> {
|
|
415
|
+
return typeof v === 'object' && v !== null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function assertNoCrossSpaceFkReverseReferences(
|
|
419
|
+
extensions: readonly CrossSpaceFkView[],
|
|
420
|
+
): void {
|
|
421
|
+
const dependsOnMap = buildTransitiveDependsOnMap(extensions);
|
|
422
|
+
|
|
423
|
+
for (const ext of extensions) {
|
|
424
|
+
const namespaces = ext.contractSpace?.contractJson?.storage?.namespaces;
|
|
425
|
+
if (!isObjectRecord(namespaces)) continue;
|
|
426
|
+
for (const ns of Object.values(namespaces)) {
|
|
427
|
+
if (!isObjectRecord(ns)) continue;
|
|
428
|
+
const entries = ns['entries'];
|
|
429
|
+
if (!isObjectRecord(entries)) continue;
|
|
430
|
+
for (const slot of Object.values(entries)) {
|
|
431
|
+
if (!isObjectRecord(slot)) continue;
|
|
432
|
+
for (const table of Object.values(slot)) {
|
|
433
|
+
if (!isObjectRecord(table)) continue;
|
|
434
|
+
const foreignKeys = table['foreignKeys'];
|
|
435
|
+
if (!Array.isArray(foreignKeys)) continue;
|
|
436
|
+
for (const fk of foreignKeys) {
|
|
437
|
+
if (!isObjectRecord(fk)) continue;
|
|
438
|
+
const target = fk['target'];
|
|
439
|
+
if (!isObjectRecord(target)) continue;
|
|
440
|
+
if (target['spaceId'] === undefined) continue;
|
|
441
|
+
const targetSpaceId = target['spaceId'];
|
|
442
|
+
if (typeof targetSpaceId !== 'string') continue;
|
|
443
|
+
// Check if targetSpaceId depends on ext.id (directly or transitively)
|
|
444
|
+
const targetDeps = dependsOnMap.get(targetSpaceId);
|
|
445
|
+
if (targetDeps?.has(ext.id)) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`Cross-space FK reverse-reference detected: extension "${ext.id}" has a cross-space FK targeting space "${targetSpaceId}", but "${targetSpaceId}" depends on "${ext.id}". Cross-space FKs must follow the dependency direction (a space can only reference spaces it depends on, not spaces that depend on it).`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
317
457
|
export function createSqlFamilyInstance<TTargetId extends string>(
|
|
318
458
|
stack: ControlStack<'sql', TTargetId>,
|
|
319
459
|
): SqlFamilyInstance {
|
|
@@ -353,6 +493,8 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
353
493
|
}
|
|
354
494
|
}
|
|
355
495
|
|
|
496
|
+
assertNoCrossSpaceFkReverseReferences(extensions);
|
|
497
|
+
|
|
356
498
|
const { codecTypeImports, extensionIds } = stack;
|
|
357
499
|
|
|
358
500
|
const typeMetadataRegistry = buildSqlTypeMetadataRegistry({
|
|
@@ -361,19 +503,11 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
361
503
|
extensionPacks: extensions,
|
|
362
504
|
});
|
|
363
505
|
|
|
364
|
-
// Family-instance methods accept `
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
const getControlAdapter = () =>
|
|
369
|
-
const controlAdapter = adapter.create(stack);
|
|
370
|
-
if (!isSqlControlAdapter(controlAdapter)) {
|
|
371
|
-
throw new Error(
|
|
372
|
-
'Adapter does not implement SqlControlAdapter (missing introspect, readMarker, or readAllMarkers)',
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
return controlAdapter;
|
|
376
|
-
};
|
|
506
|
+
// Family-instance methods accept `SqlControlDriverInstance<string>` — the
|
|
507
|
+
// family API isn't generic on the target id. The adapter descriptor's `create`
|
|
508
|
+
// returns the concrete `SqlControlAdapter<TTargetId>`; widening the target id to
|
|
509
|
+
// `string` here matches the family-level driver type without a per-method probe.
|
|
510
|
+
const getControlAdapter = (): SqlControlAdapter<string> => adapter.create(stack);
|
|
377
511
|
|
|
378
512
|
const targetSerializer = (
|
|
379
513
|
target as unknown as {
|
|
@@ -396,7 +530,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
396
530
|
},
|
|
397
531
|
|
|
398
532
|
async verify(verifyOptions: {
|
|
399
|
-
readonly driver:
|
|
533
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
400
534
|
readonly contract: unknown;
|
|
401
535
|
readonly expectedTargetId: string;
|
|
402
536
|
readonly contractPath: string;
|
|
@@ -544,7 +678,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
544
678
|
});
|
|
545
679
|
},
|
|
546
680
|
async sign(options: {
|
|
547
|
-
readonly driver:
|
|
681
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
548
682
|
readonly contract: unknown;
|
|
549
683
|
readonly contractPath: string;
|
|
550
684
|
readonly configPath?: string;
|
|
@@ -561,24 +695,24 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
561
695
|
: contractStorageHash;
|
|
562
696
|
const contractTarget = contract.target;
|
|
563
697
|
|
|
564
|
-
|
|
565
|
-
|
|
698
|
+
const controlAdapter = getControlAdapter();
|
|
699
|
+
const lowererContext = { contract };
|
|
700
|
+
for (const query of controlAdapter.bootstrapSignMarkerQueries()) {
|
|
701
|
+
const lowered = controlAdapter.lower(query, lowererContext);
|
|
702
|
+
await driver.query(lowered.sql, lowered.params);
|
|
703
|
+
}
|
|
566
704
|
|
|
567
|
-
const existingMarker = await
|
|
705
|
+
const existingMarker = await controlAdapter.readMarker(driver, APP_SPACE_ID);
|
|
568
706
|
|
|
569
707
|
let markerCreated = false;
|
|
570
708
|
let markerUpdated = false;
|
|
571
709
|
let previousHashes: { storageHash?: string; profileHash?: string } | undefined;
|
|
572
710
|
|
|
573
711
|
if (!existingMarker) {
|
|
574
|
-
|
|
575
|
-
space: APP_SPACE_ID,
|
|
712
|
+
await controlAdapter.insertMarker(driver, APP_SPACE_ID, {
|
|
576
713
|
storageHash: contractStorageHash,
|
|
577
714
|
profileHash: contractProfileHash,
|
|
578
|
-
contractJson: contractInput,
|
|
579
|
-
canonicalVersion: 1,
|
|
580
715
|
});
|
|
581
|
-
await driver.query(write.insert.sql, write.insert.params);
|
|
582
716
|
markerCreated = true;
|
|
583
717
|
} else {
|
|
584
718
|
const existingStorageHash = existingMarker.storageHash;
|
|
@@ -592,14 +726,18 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
592
726
|
storageHash: existingStorageHash,
|
|
593
727
|
profileHash: existingProfileHash,
|
|
594
728
|
};
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
729
|
+
const updated = await controlAdapter.updateMarker(
|
|
730
|
+
driver,
|
|
731
|
+
APP_SPACE_ID,
|
|
732
|
+
existingStorageHash,
|
|
733
|
+
{
|
|
734
|
+
storageHash: contractStorageHash,
|
|
735
|
+
profileHash: contractProfileHash,
|
|
736
|
+
},
|
|
737
|
+
);
|
|
738
|
+
if (!updated) {
|
|
739
|
+
throw new Error('CAS conflict: marker was modified by another process during sign');
|
|
740
|
+
}
|
|
603
741
|
markerUpdated = true;
|
|
604
742
|
}
|
|
605
743
|
}
|
|
@@ -641,18 +779,66 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
641
779
|
};
|
|
642
780
|
},
|
|
643
781
|
async readMarker(options: {
|
|
644
|
-
readonly driver:
|
|
782
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
645
783
|
readonly space: string;
|
|
646
784
|
}): Promise<ContractMarkerRecord | null> {
|
|
647
785
|
return getControlAdapter().readMarker(options.driver, options.space);
|
|
648
786
|
},
|
|
649
787
|
async readAllMarkers(options: {
|
|
650
|
-
readonly driver:
|
|
788
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
651
789
|
}): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
|
|
652
790
|
return getControlAdapter().readAllMarkers(options.driver);
|
|
653
791
|
},
|
|
792
|
+
async readLedger(options: {
|
|
793
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
794
|
+
readonly space?: string;
|
|
795
|
+
}): Promise<readonly LedgerEntryRecord[]> {
|
|
796
|
+
return getControlAdapter().readLedger(options.driver, options.space);
|
|
797
|
+
},
|
|
798
|
+
async initMarker(options: {
|
|
799
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
800
|
+
readonly space: string;
|
|
801
|
+
readonly destination: {
|
|
802
|
+
readonly storageHash: string;
|
|
803
|
+
readonly profileHash: string;
|
|
804
|
+
readonly invariants?: readonly string[];
|
|
805
|
+
};
|
|
806
|
+
}): Promise<void> {
|
|
807
|
+
return getControlAdapter().initMarker(options.driver, options.space, options.destination);
|
|
808
|
+
},
|
|
809
|
+
async updateMarker(options: {
|
|
810
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
811
|
+
readonly space: string;
|
|
812
|
+
readonly expectedFrom: string;
|
|
813
|
+
readonly destination: {
|
|
814
|
+
readonly storageHash: string;
|
|
815
|
+
readonly profileHash: string;
|
|
816
|
+
readonly invariants?: readonly string[];
|
|
817
|
+
};
|
|
818
|
+
}): Promise<boolean> {
|
|
819
|
+
return getControlAdapter().updateMarker(
|
|
820
|
+
options.driver,
|
|
821
|
+
options.space,
|
|
822
|
+
options.expectedFrom,
|
|
823
|
+
options.destination,
|
|
824
|
+
);
|
|
825
|
+
},
|
|
826
|
+
async writeLedgerEntry(options: {
|
|
827
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
828
|
+
readonly space: string;
|
|
829
|
+
readonly entry: {
|
|
830
|
+
readonly edgeId: string;
|
|
831
|
+
readonly from: string;
|
|
832
|
+
readonly to: string;
|
|
833
|
+
readonly migrationName: string;
|
|
834
|
+
readonly migrationHash: string;
|
|
835
|
+
readonly operations: readonly unknown[];
|
|
836
|
+
};
|
|
837
|
+
}): Promise<void> {
|
|
838
|
+
return getControlAdapter().writeLedgerEntry(options.driver, options.space, options.entry);
|
|
839
|
+
},
|
|
654
840
|
async introspect(options: {
|
|
655
|
-
readonly driver:
|
|
841
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
656
842
|
readonly contract?: unknown;
|
|
657
843
|
}): Promise<SqlSchemaIR> {
|
|
658
844
|
return getControlAdapter().introspect(options.driver, options.contract);
|
|
@@ -662,10 +848,18 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
662
848
|
return sqlSchemaIrToPslAst(schemaIR);
|
|
663
849
|
},
|
|
664
850
|
|
|
665
|
-
lowerAst(ast: AnyQueryAst, context: LowererContext<unknown>): LoweredStatement {
|
|
851
|
+
lowerAst(ast: AnyQueryAst | DdlNode, context: LowererContext<unknown>): LoweredStatement {
|
|
666
852
|
return getControlAdapter().lower(ast, context);
|
|
667
853
|
},
|
|
668
854
|
|
|
855
|
+
bootstrapControlTableQueries(): readonly DdlNode[] {
|
|
856
|
+
return getControlAdapter().bootstrapControlTableQueries();
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
bootstrapSignMarkerQueries(): readonly DdlNode[] {
|
|
860
|
+
return getControlAdapter().bootstrapSignMarkerQueries();
|
|
861
|
+
},
|
|
862
|
+
|
|
669
863
|
toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview {
|
|
670
864
|
return sqlOperationsToPreview(operations);
|
|
671
865
|
},
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
SqlUnboundNamespace,
|
|
17
17
|
StorageTable,
|
|
18
18
|
type StorageTableInput,
|
|
19
|
+
StorageValueSet,
|
|
20
|
+
type StorageValueSetInput,
|
|
19
21
|
} from '@prisma-next/sql-contract/types';
|
|
20
22
|
import {
|
|
21
23
|
createSqlContractSchema,
|
|
@@ -29,16 +31,16 @@ import { type Type, type } from 'arktype';
|
|
|
29
31
|
const NamespaceRawSchema = type({
|
|
30
32
|
id: 'string',
|
|
31
33
|
'kind?': 'string',
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
entries: type({
|
|
35
|
+
'+': 'ignore',
|
|
36
|
+
}),
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
38
40
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
export type SqlEntityHydrationFactory = (entry: unknown) =>
|
|
43
|
+
export type SqlEntityHydrationFactory = (entry: unknown) => unknown;
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* SQL family `ContractSerializer` abstract base. Carries the SQL-shared
|
|
@@ -70,7 +72,10 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
70
72
|
private readonly contractSchema: Type<unknown> | undefined;
|
|
71
73
|
|
|
72
74
|
constructor(
|
|
73
|
-
|
|
75
|
+
protected readonly entityTypeRegistry: ReadonlyMap<
|
|
76
|
+
string,
|
|
77
|
+
SqlEntityHydrationFactory
|
|
78
|
+
> = new Map(),
|
|
74
79
|
validatorFragments?: ReadonlyMap<string, Type<unknown>>,
|
|
75
80
|
) {
|
|
76
81
|
// Only build a fragments-aware contract schema when pack contributions
|
|
@@ -158,7 +163,12 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
158
163
|
const namespaceMaterialised =
|
|
159
164
|
namespaceHydrated instanceof NamespaceBase
|
|
160
165
|
? namespaceHydrated
|
|
161
|
-
: buildSqlNamespace(
|
|
166
|
+
: buildSqlNamespace(
|
|
167
|
+
blindCast<
|
|
168
|
+
SqlNamespaceTablesInput,
|
|
169
|
+
'hydrateSqlNamespaceEntry returns SqlNamespaceTablesInput when raw is not a NamespaceBase'
|
|
170
|
+
>(namespaceHydrated),
|
|
171
|
+
);
|
|
162
172
|
return [nsId, namespaceMaterialised];
|
|
163
173
|
}),
|
|
164
174
|
);
|
|
@@ -172,69 +182,72 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
172
182
|
return raw;
|
|
173
183
|
}
|
|
174
184
|
const rawRecord = isPlainRecord(raw) ? raw : {};
|
|
185
|
+
if (
|
|
186
|
+
Object.hasOwn(rawRecord, 'tables') ||
|
|
187
|
+
Object.hasOwn(rawRecord, 'enum') ||
|
|
188
|
+
Object.hasOwn(rawRecord, 'collections')
|
|
189
|
+
) {
|
|
190
|
+
throw new ContractValidationError(
|
|
191
|
+
'Namespace envelope uses deprecated flat slot keys; expected `entries: { table? }`',
|
|
192
|
+
'structural',
|
|
193
|
+
);
|
|
194
|
+
}
|
|
175
195
|
const id = typeof rawRecord['id'] === 'string' ? rawRecord['id'] : nsId;
|
|
176
196
|
const parsed = NamespaceRawSchema({ ...rawRecord, id });
|
|
177
197
|
if (parsed instanceof type.errors) {
|
|
178
198
|
const messages = parsed.map((p: { message: string }) => p.message).join('; ');
|
|
179
199
|
throw new ContractValidationError(`Namespace hydration failed: ${messages}`, 'structural');
|
|
180
200
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
201
|
+
// Default to empty table; overwritten below if raw entries carry a table slot.
|
|
202
|
+
const entriesInput: {
|
|
203
|
+
table: Record<string, StorageTable>;
|
|
204
|
+
valueSet?: Record<string, StorageValueSet>;
|
|
205
|
+
} = { table: {} };
|
|
206
|
+
const entriesRaw = parsed.entries;
|
|
207
|
+
if (entriesRaw !== undefined && typeof entriesRaw === 'object' && entriesRaw !== null) {
|
|
208
|
+
const rawEntries = entriesRaw as Record<string, unknown>;
|
|
209
|
+
const tableSlot = rawEntries['table'];
|
|
210
|
+
if (tableSlot !== null && typeof tableSlot === 'object' && !Array.isArray(tableSlot)) {
|
|
211
|
+
entriesInput.table = Object.fromEntries(
|
|
212
|
+
Object.entries(tableSlot as Record<string, unknown>).map(([tableName, table]) => [
|
|
190
213
|
tableName,
|
|
191
214
|
table instanceof StorageTable ? table : new StorageTable(table as StorageTableInput),
|
|
192
215
|
]),
|
|
193
216
|
);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const hydratedSlot = Object.fromEntries(
|
|
198
|
-
Object.entries(slotValue as Record<string, unknown>).map(([entryName, entry]) => {
|
|
199
|
-
if (typeof entry !== 'object' || entry === null) {
|
|
200
|
-
return [entryName, entry];
|
|
201
|
-
}
|
|
202
|
-
const kind = (entry as { kind?: unknown }).kind;
|
|
203
|
-
if (typeof kind === 'string') {
|
|
204
|
-
const factory = this.entityTypeRegistry.get(kind);
|
|
205
|
-
if (factory !== undefined) {
|
|
206
|
-
return [entryName, factory(entry)];
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return [entryName, entry];
|
|
210
|
-
}),
|
|
211
|
-
);
|
|
212
|
-
if (Object.keys(hydratedSlot).length > 0) {
|
|
213
|
-
result[propertyKey] = hydratedSlot;
|
|
214
217
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
218
|
+
const valueSetSlot = rawEntries['valueSet'];
|
|
219
|
+
if (
|
|
220
|
+
valueSetSlot !== null &&
|
|
221
|
+
typeof valueSetSlot === 'object' &&
|
|
222
|
+
!Array.isArray(valueSetSlot)
|
|
223
|
+
) {
|
|
224
|
+
entriesInput.valueSet = Object.fromEntries(
|
|
225
|
+
Object.entries(
|
|
226
|
+
blindCast<
|
|
227
|
+
Record<string, unknown>,
|
|
228
|
+
'valueSet slot is a plain record after object check'
|
|
229
|
+
>(valueSetSlot),
|
|
230
|
+
).map(([vsName, vs]) => [
|
|
231
|
+
vsName,
|
|
232
|
+
vs instanceof StorageValueSet
|
|
233
|
+
? vs
|
|
234
|
+
: new StorageValueSet(
|
|
235
|
+
blindCast<
|
|
236
|
+
StorageValueSetInput,
|
|
237
|
+
'non-instance valueSet entry is StorageValueSetInput'
|
|
238
|
+
>(vs),
|
|
239
|
+
),
|
|
240
|
+
]),
|
|
241
|
+
);
|
|
228
242
|
}
|
|
243
|
+
// Target-specific slots (e.g. postgres `type`) are left for target
|
|
244
|
+
// overrides to extract from the original `raw` parameter.
|
|
229
245
|
}
|
|
230
246
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
tables,
|
|
236
|
-
...(enumSlot !== undefined ? { enum: enumSlot } : {}),
|
|
237
|
-
} as SqlNamespaceTablesInput;
|
|
247
|
+
return blindCast<SqlNamespaceTablesInput, 'hydrated namespace tables input'>({
|
|
248
|
+
id,
|
|
249
|
+
entries: entriesInput,
|
|
250
|
+
});
|
|
238
251
|
}
|
|
239
252
|
|
|
240
253
|
protected hydrateStorageTypeEntry(entry: SqlStorageTypeEntry): SqlStorageTypeEntry {
|
|
@@ -249,7 +262,10 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
249
262
|
if (factory === undefined) {
|
|
250
263
|
return entry;
|
|
251
264
|
}
|
|
252
|
-
return
|
|
265
|
+
return blindCast<
|
|
266
|
+
SqlStorageTypeEntry,
|
|
267
|
+
'entity registry factory returns SqlStorageTypeEntry for storage.types entries'
|
|
268
|
+
>(factory(entry));
|
|
253
269
|
}
|
|
254
270
|
|
|
255
271
|
protected constructTargetContract(hydrated: Contract<SqlStorage>): TContract {
|