@prisma-next/family-sql 0.16.0-dev.3 → 0.16.0-dev.31
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/control.d.mts +1 -1
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +108 -33
- package/dist/control.mjs.map +1 -1
- package/dist/diff.d.mts +1 -1
- package/dist/errors-ZGKTAYsm.mjs +9 -0
- package/dist/errors-ZGKTAYsm.mjs.map +1 -0
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +1 -1
- package/dist/migration.d.mts +1 -1
- package/dist/psl-infer.d.mts +3 -5
- package/dist/psl-infer.d.mts.map +1 -1
- package/dist/psl-infer.mjs +11 -8
- package/dist/psl-infer.mjs.map +1 -1
- package/dist/{sql-contract-serializer-C75cfMSS.mjs → sql-contract-serializer-othnJUZ_.mjs} +7 -2
- package/dist/sql-contract-serializer-othnJUZ_.mjs.map +1 -0
- package/dist/{types-6JMCRorL.d.mts → types-DhAmOoxA.d.mts} +11 -4
- package/dist/{types-6JMCRorL.d.mts.map → types-DhAmOoxA.d.mts.map} +1 -1
- package/dist/{verify-C-G0obRm.mjs → verify-DuuJFiTm.mjs} +7 -2
- package/dist/verify-DuuJFiTm.mjs.map +1 -0
- package/dist/verify.d.mts.map +1 -1
- package/dist/verify.mjs +1 -1
- package/package.json +21 -21
- package/src/core/control-instance.ts +50 -24
- package/src/core/errors.ts +22 -0
- package/src/core/ir/sql-contract-serializer-base.ts +8 -1
- package/src/core/migrations/contract-to-schema-ir.ts +78 -17
- package/src/core/migrations/types.ts +10 -3
- package/src/core/psl-contract-infer/name-transforms.ts +0 -12
- package/src/core/psl-contract-infer/printer-config.ts +2 -3
- package/src/core/psl-contract-infer/relation-inference.ts +23 -1
- package/src/core/verify.ts +10 -1
- package/src/exports/psl-infer.ts +1 -2
- package/dist/sql-contract-serializer-C75cfMSS.mjs.map +0 -1
- package/dist/verify-C-G0obRm.mjs.map +0 -1
|
@@ -41,10 +41,11 @@ import type {
|
|
|
41
41
|
LowererContext,
|
|
42
42
|
SqlExecuteRequest,
|
|
43
43
|
} from '@prisma-next/sql-relational-core/ast';
|
|
44
|
-
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
45
44
|
import type { SqlSchemaIRNode, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
|
|
46
45
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
47
46
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
47
|
+
import { InternalError } from '@prisma-next/utils/internal-error';
|
|
48
|
+
import type { StructuredError } from '@prisma-next/utils/structured-error';
|
|
48
49
|
import type { SqlControlAdapter } from './control-adapter';
|
|
49
50
|
import type {
|
|
50
51
|
SqlControlTargetDescriptor,
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
classifyDiffSubjectGranularity,
|
|
56
57
|
verifySqlSchemaByDiff,
|
|
57
58
|
} from './diff/schema-verify';
|
|
59
|
+
import { sqlFamilyError } from './errors';
|
|
58
60
|
import { SqlContractSerializer } from './ir/sql-contract-serializer';
|
|
59
61
|
import type { SqlSchemaDiffFn } from './migrations/schema-differ';
|
|
60
62
|
import type {
|
|
@@ -64,6 +66,18 @@ import type {
|
|
|
64
66
|
import { sqlOperationsToPreview } from './operation-preview';
|
|
65
67
|
import { collectSupportedCodecTypeIds } from './verify';
|
|
66
68
|
|
|
69
|
+
function missingDescriptorOperationError(targetId: string, operation: string): StructuredError {
|
|
70
|
+
return sqlFamilyError(
|
|
71
|
+
'CONTRACT.PACK_CONTRIBUTION_INVALID',
|
|
72
|
+
`SQL target "${targetId}" is missing the required ${operation} descriptor operation`,
|
|
73
|
+
{
|
|
74
|
+
why: `The target descriptor does not contribute the ${operation} operation the SQL family requires for this call.`,
|
|
75
|
+
fix: `Use a target package whose control descriptor implements ${operation}.`,
|
|
76
|
+
meta: { targetId, operation },
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
function extractCodecTypeIdsFromContract(contract: unknown): readonly string[] {
|
|
68
82
|
const typeIds = new Set<string>();
|
|
69
83
|
|
|
@@ -350,9 +364,9 @@ interface DescriptorWithStorageTypes {
|
|
|
350
364
|
function buildSqlTypeMetadataRegistry(options: {
|
|
351
365
|
readonly target: DescriptorWithStorageTypes;
|
|
352
366
|
readonly adapter: DescriptorWithStorageTypes & { readonly targetId: string };
|
|
353
|
-
readonly
|
|
367
|
+
readonly extensions: readonly DescriptorWithStorageTypes[];
|
|
354
368
|
}): SqlTypeMetadataRegistry {
|
|
355
|
-
const { target, adapter,
|
|
369
|
+
const { target, adapter, extensions } = options;
|
|
356
370
|
const registry = new Map<string, SqlTypeMetadata>();
|
|
357
371
|
const targetId = adapter.targetId;
|
|
358
372
|
const descriptors = [target, adapter, ...extensions];
|
|
@@ -384,7 +398,7 @@ interface CrossSpaceFkView {
|
|
|
384
398
|
readonly id: string;
|
|
385
399
|
readonly contractSpace?: {
|
|
386
400
|
readonly contractJson?: {
|
|
387
|
-
readonly
|
|
401
|
+
readonly extensions?: Readonly<Record<string, unknown>>;
|
|
388
402
|
readonly storage?: {
|
|
389
403
|
readonly namespaces?: Readonly<Record<string, unknown>>;
|
|
390
404
|
};
|
|
@@ -402,7 +416,7 @@ function buildTransitiveDependsOnMap(
|
|
|
402
416
|
): Map<string, Set<string>> {
|
|
403
417
|
const directDeps = new Map<string, readonly string[]>();
|
|
404
418
|
for (const ext of extensions) {
|
|
405
|
-
const packs = ext.contractSpace?.contractJson?.
|
|
419
|
+
const packs = ext.contractSpace?.contractJson?.extensions;
|
|
406
420
|
const deps = packs !== null && typeof packs === 'object' ? Object.keys(packs) : [];
|
|
407
421
|
directDeps.set(ext.id, deps);
|
|
408
422
|
}
|
|
@@ -476,8 +490,14 @@ export function assertNoCrossSpaceFkReverseReferences(
|
|
|
476
490
|
// Check if targetSpaceId depends on ext.id (directly or transitively)
|
|
477
491
|
const targetDeps = dependsOnMap.get(targetSpaceId);
|
|
478
492
|
if (targetDeps?.has(ext.id)) {
|
|
479
|
-
throw
|
|
493
|
+
throw sqlFamilyError(
|
|
494
|
+
'CONTRACT.FOREIGN_KEY_INVALID',
|
|
480
495
|
`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).`,
|
|
496
|
+
{
|
|
497
|
+
why: 'The foreign key points against the contract-space dependency direction.',
|
|
498
|
+
fix: 'Move the foreign key to the depending space, or restructure the extension dependencies so the referencing space depends on the referenced one.',
|
|
499
|
+
meta: { extensionId: ext.id, targetSpaceId },
|
|
500
|
+
},
|
|
481
501
|
);
|
|
482
502
|
}
|
|
483
503
|
}
|
|
@@ -491,7 +511,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
491
511
|
stack: ControlStack<'sql', TTargetId>,
|
|
492
512
|
): SqlFamilyInstance {
|
|
493
513
|
if (!stack.adapter) {
|
|
494
|
-
throw new
|
|
514
|
+
throw new InternalError('SQL family requires an adapter descriptor in ControlStack');
|
|
495
515
|
}
|
|
496
516
|
|
|
497
517
|
const target = stack.target as unknown as TargetDescriptor<'sql', TTargetId> &
|
|
@@ -499,7 +519,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
499
519
|
const adapter = stack.adapter as unknown as SqlControlAdapterDescriptor<TTargetId> &
|
|
500
520
|
DescriptorWithStorageTypes;
|
|
501
521
|
const extensions =
|
|
502
|
-
stack.
|
|
522
|
+
stack.extensions as unknown as readonly (SqlControlExtensionDescriptor<TTargetId> &
|
|
503
523
|
DescriptorWithStorageTypes)[];
|
|
504
524
|
|
|
505
525
|
// Descriptor self-consistency check.
|
|
@@ -533,7 +553,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
533
553
|
const typeMetadataRegistry = buildSqlTypeMetadataRegistry({
|
|
534
554
|
target,
|
|
535
555
|
adapter,
|
|
536
|
-
|
|
556
|
+
extensions: extensions,
|
|
537
557
|
});
|
|
538
558
|
|
|
539
559
|
// Lazily construct the control adapter on first use, then memoize it.
|
|
@@ -759,14 +779,10 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
759
779
|
}): VerifyDatabaseSchemaResult {
|
|
760
780
|
const contract = deserializeWithTargetSerializer(options.contract) as Contract<SqlStorage>;
|
|
761
781
|
if (!diffSchema) {
|
|
762
|
-
throw
|
|
763
|
-
`SQL target "${target.targetId}" is missing the required diffSchema descriptor operation`,
|
|
764
|
-
);
|
|
782
|
+
throw missingDescriptorOperationError(target.targetId, 'diffSchema');
|
|
765
783
|
}
|
|
766
784
|
if (!targetGranularityOf) {
|
|
767
|
-
throw
|
|
768
|
-
`SQL target "${target.targetId}" is missing the required classifySubjectGranularity descriptor operation`,
|
|
769
|
-
);
|
|
785
|
+
throw missingDescriptorOperationError(target.targetId, 'classifySubjectGranularity');
|
|
770
786
|
}
|
|
771
787
|
// THE VERDICT: the target's full-tree node diff, graded by the
|
|
772
788
|
// family's post-diff filters (strict gating + control-policy
|
|
@@ -793,9 +809,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
793
809
|
*/
|
|
794
810
|
classifySubjectGranularity(issue: SchemaDiffIssue): DiffSubjectGranularity | undefined {
|
|
795
811
|
if (!targetGranularityOf) {
|
|
796
|
-
throw
|
|
797
|
-
`SQL target "${target.targetId}" is missing the required classifySubjectGranularity descriptor operation`,
|
|
798
|
-
);
|
|
812
|
+
throw missingDescriptorOperationError(target.targetId, 'classifySubjectGranularity');
|
|
799
813
|
}
|
|
800
814
|
return classifyDiffSubjectGranularity(issue, targetGranularityOf);
|
|
801
815
|
},
|
|
@@ -808,9 +822,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
808
822
|
*/
|
|
809
823
|
classifyEntityKind(issue: SchemaDiffIssue): string | undefined {
|
|
810
824
|
if (!targetEntityKindOf) {
|
|
811
|
-
throw
|
|
812
|
-
`SQL target "${target.targetId}" is missing the required classifyEntityKind descriptor operation`,
|
|
813
|
-
);
|
|
825
|
+
throw missingDescriptorOperationError(target.targetId, 'classifyEntityKind');
|
|
814
826
|
}
|
|
815
827
|
return classifyDiffEntityKind(issue, targetEntityKindOf);
|
|
816
828
|
},
|
|
@@ -873,7 +885,15 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
873
885
|
},
|
|
874
886
|
);
|
|
875
887
|
if (!updated) {
|
|
876
|
-
throw
|
|
888
|
+
throw sqlFamilyError(
|
|
889
|
+
'MIGRATION.MARKER_CAS_FAILURE',
|
|
890
|
+
'CAS conflict: marker was modified by another process during sign',
|
|
891
|
+
{
|
|
892
|
+
why: 'Another process updated the contract marker between the read and the compare-and-swap write.',
|
|
893
|
+
fix: 'Re-run the sign command; if it keeps failing, make sure only one migration process runs at a time.',
|
|
894
|
+
meta: { space: APP_SPACE_ID },
|
|
895
|
+
},
|
|
896
|
+
);
|
|
877
897
|
}
|
|
878
898
|
markerUpdated = true;
|
|
879
899
|
}
|
|
@@ -984,8 +1004,14 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
984
1004
|
|
|
985
1005
|
inferPslContract(schemaIR: SqlSchemaIRNode): PslDocumentAst {
|
|
986
1006
|
if (!targetInferPslContract) {
|
|
987
|
-
throw
|
|
1007
|
+
throw sqlFamilyError(
|
|
1008
|
+
'CONTRACT.INFER_UNSUPPORTED',
|
|
988
1009
|
`Target "${target.targetId}" does not support contract infer (no inferPslContract on its descriptor).`,
|
|
1010
|
+
{
|
|
1011
|
+
why: 'The target descriptor does not provide the inferPslContract hook, so a PSL contract cannot be inferred from the database schema.',
|
|
1012
|
+
fix: 'Use a target package that supports contract infer, or author the contract instead of inferring it.',
|
|
1013
|
+
meta: { targetId: target.targetId },
|
|
1014
|
+
},
|
|
989
1015
|
);
|
|
990
1016
|
}
|
|
991
1017
|
return targetInferPslContract(schemaIR, describedContracts);
|
|
@@ -1112,7 +1138,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
1112
1138
|
}
|
|
1113
1139
|
|
|
1114
1140
|
for (const index of table.indexes) {
|
|
1115
|
-
const name = index.name
|
|
1141
|
+
const name = index.name;
|
|
1116
1142
|
const label = index.unique ? `unique index ${name}` : `index ${name}`;
|
|
1117
1143
|
children.push(
|
|
1118
1144
|
new SchemaTreeNode({
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StructuredError, StructuredErrorOptions } from '@prisma-next/utils/structured-error';
|
|
2
|
+
import { structuredError } from '@prisma-next/utils/structured-error';
|
|
3
|
+
|
|
4
|
+
type SqlFamilyErrorCode =
|
|
5
|
+
| 'CONTRACT.ENUM_INVALID'
|
|
6
|
+
| 'CONTRACT.ENUM_UNKNOWN'
|
|
7
|
+
| 'CONTRACT.FOREIGN_KEY_INVALID'
|
|
8
|
+
| 'CONTRACT.INFER_UNSUPPORTED'
|
|
9
|
+
| 'CONTRACT.MARKER_ROW_CORRUPT'
|
|
10
|
+
| 'CONTRACT.NAMESPACE_UNKNOWN'
|
|
11
|
+
| 'CONTRACT.PACK_CONTRIBUTION_INVALID'
|
|
12
|
+
| 'CONTRACT.TABLE_AMBIGUOUS'
|
|
13
|
+
| 'CONTRACT.TYPE_UNKNOWN'
|
|
14
|
+
| 'MIGRATION.MARKER_CAS_FAILURE';
|
|
15
|
+
|
|
16
|
+
export function sqlFamilyError(
|
|
17
|
+
code: SqlFamilyErrorCode,
|
|
18
|
+
message: string,
|
|
19
|
+
options?: StructuredErrorOptions,
|
|
20
|
+
): StructuredError {
|
|
21
|
+
return structuredError(code, message, options);
|
|
22
|
+
}
|
|
@@ -24,6 +24,7 @@ import { blindCast } from '@prisma-next/utils/casts';
|
|
|
24
24
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
25
25
|
import type { JsonObject, JsonValue } from '@prisma-next/utils/json';
|
|
26
26
|
import { type Type, type } from 'arktype';
|
|
27
|
+
import { sqlFamilyError } from '../errors';
|
|
27
28
|
|
|
28
29
|
const NamespaceRawSchema = type({
|
|
29
30
|
id: 'string',
|
|
@@ -144,8 +145,14 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
144
145
|
Object.entries(namespaces).map(([nsId, namespaceEntryRaw]) => {
|
|
145
146
|
const namespaceHydrated = this.hydrateSqlNamespaceEntry(nsId, namespaceEntryRaw);
|
|
146
147
|
if (!isMaterializedSqlNamespace(namespaceHydrated)) {
|
|
147
|
-
throw
|
|
148
|
+
throw sqlFamilyError(
|
|
149
|
+
'CONTRACT.PACK_CONTRIBUTION_INVALID',
|
|
148
150
|
`Target serializer bug: hydrateSqlNamespaceEntry for namespace "${nsId}" returned a non-NamespaceBase value. Override hydrateSqlNamespaceEntry to produce a target namespace concretion.`,
|
|
151
|
+
{
|
|
152
|
+
why: 'The target contract serializer hydrated a namespace into a value that is not a materialized namespace concretion.',
|
|
153
|
+
fix: 'Fix the target package: its hydrateSqlNamespaceEntry override must return a target namespace concretion.',
|
|
154
|
+
meta: { namespaceId: nsId },
|
|
155
|
+
},
|
|
149
156
|
);
|
|
150
157
|
}
|
|
151
158
|
return [nsId, namespaceHydrated];
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
} from '@prisma-next/sql-schema-ir/types';
|
|
29
29
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
30
30
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
31
|
+
import { InternalError } from '@prisma-next/utils/internal-error';
|
|
32
|
+
import { sqlFamilyError } from '../errors';
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* Target-specific callback that expands a column's base `nativeType` and optional
|
|
@@ -187,8 +189,14 @@ function resolveColumnTypeMetadata(
|
|
|
187
189
|
}
|
|
188
190
|
const referenced = storageTypes[column.typeRef];
|
|
189
191
|
if (!referenced) {
|
|
190
|
-
throw
|
|
192
|
+
throw sqlFamilyError(
|
|
193
|
+
'CONTRACT.TYPE_UNKNOWN',
|
|
191
194
|
`Column references storage type "${column.typeRef}" but it is not defined in storage.types.`,
|
|
195
|
+
{
|
|
196
|
+
why: 'The column typeRef does not resolve to any entry in the contract storage.types map.',
|
|
197
|
+
fix: 'Regenerate the contract from its authoring source; do not hand-edit contract JSON.',
|
|
198
|
+
meta: { typeRef: column.typeRef },
|
|
199
|
+
},
|
|
192
200
|
);
|
|
193
201
|
}
|
|
194
202
|
if (isStorageTypeInstance(referenced)) {
|
|
@@ -198,7 +206,7 @@ function resolveColumnTypeMetadata(
|
|
|
198
206
|
typeParams: referenced.typeParams,
|
|
199
207
|
};
|
|
200
208
|
}
|
|
201
|
-
throw new
|
|
209
|
+
throw new InternalError(
|
|
202
210
|
`Storage type "${column.typeRef}" has an unknown polymorphic kind; expected a codec-typed StorageTypeInstance.`,
|
|
203
211
|
);
|
|
204
212
|
}
|
|
@@ -224,14 +232,26 @@ export function resolveValueSetValues(
|
|
|
224
232
|
): readonly string[] {
|
|
225
233
|
const ns = storage.namespaces[ref.namespaceId];
|
|
226
234
|
if (!ns) {
|
|
227
|
-
throw
|
|
235
|
+
throw sqlFamilyError(
|
|
236
|
+
'CONTRACT.NAMESPACE_UNKNOWN',
|
|
228
237
|
`resolveValueSetValues: namespace "${ref.namespaceId}" not found in storage (${contextLabel})`,
|
|
238
|
+
{
|
|
239
|
+
why: 'A value-set reference names a namespace the contract storage does not declare.',
|
|
240
|
+
fix: 'Regenerate the contract from its authoring source; do not hand-edit contract JSON.',
|
|
241
|
+
meta: { namespaceId: ref.namespaceId, context: contextLabel },
|
|
242
|
+
},
|
|
229
243
|
);
|
|
230
244
|
}
|
|
231
245
|
const valueSet = ns.entries.valueSet?.[ref.entityName];
|
|
232
246
|
if (!valueSet) {
|
|
233
|
-
throw
|
|
247
|
+
throw sqlFamilyError(
|
|
248
|
+
'CONTRACT.ENUM_UNKNOWN',
|
|
234
249
|
`resolveValueSetValues: value-set "${ref.entityName}" not found in namespace "${ref.namespaceId}" (${contextLabel})`,
|
|
250
|
+
{
|
|
251
|
+
why: 'A value-set reference names an entry its namespace does not declare.',
|
|
252
|
+
fix: 'Regenerate the contract from its authoring source; do not hand-edit contract JSON.',
|
|
253
|
+
meta: { valueSet: ref.entityName, namespaceId: ref.namespaceId, context: contextLabel },
|
|
254
|
+
},
|
|
235
255
|
);
|
|
236
256
|
}
|
|
237
257
|
// Only TEXT enums ship a CHECK-constraint round-trip in this slice. A
|
|
@@ -239,8 +259,14 @@ export function resolveValueSetValues(
|
|
|
239
259
|
// is future work; fail loudly rather than emit a wrong numeric-as-text check.
|
|
240
260
|
const values = valueSet.values;
|
|
241
261
|
if (!allStrings(values)) {
|
|
242
|
-
throw
|
|
262
|
+
throw sqlFamilyError(
|
|
263
|
+
'CONTRACT.ENUM_INVALID',
|
|
243
264
|
`resolveValueSetValues: value-set "${ref.entityName}" in namespace "${ref.namespaceId}" has a non-string value; numeric-enum CHECK constraints are not yet supported (${contextLabel})`,
|
|
265
|
+
{
|
|
266
|
+
why: 'Only string value-sets can be rendered as CHECK constraints; this value-set carries a non-string value.',
|
|
267
|
+
fix: 'Use string enum values, or drop the CHECK-constrained enum until numeric enums are supported.',
|
|
268
|
+
meta: { valueSet: ref.entityName, namespaceId: ref.namespaceId, context: contextLabel },
|
|
269
|
+
},
|
|
244
270
|
);
|
|
245
271
|
}
|
|
246
272
|
return values;
|
|
@@ -276,17 +302,30 @@ function convertUnique(unique: UniqueConstraint, tableName: string): SqlUniqueIR
|
|
|
276
302
|
};
|
|
277
303
|
}
|
|
278
304
|
|
|
279
|
-
function convertIndex(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
305
|
+
function convertIndex(
|
|
306
|
+
index: Index,
|
|
307
|
+
tableName: string,
|
|
308
|
+
tableColumns: readonly string[],
|
|
309
|
+
): SqlIndexIRInput {
|
|
310
|
+
const base = {
|
|
311
|
+
name: index.name,
|
|
312
|
+
prefix: index.prefix,
|
|
313
|
+
where: index.where,
|
|
314
|
+
unique: index.unique,
|
|
315
|
+
partial: index.where !== undefined,
|
|
284
316
|
// Carried so the derived index node compares type/options against the
|
|
285
317
|
// introspected side (the legacy walk read them from the contract).
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
318
|
+
type: index.type,
|
|
319
|
+
options: index.options,
|
|
320
|
+
annotations: undefined,
|
|
321
|
+
// An expression index's chains cover every column of its table — the
|
|
322
|
+
// opaque expression is never parsed, so this deterministic
|
|
323
|
+
// over-approximation keeps drops ordered without a SQL parser.
|
|
324
|
+
dependsOn: flatColumnDependsOn(tableName, index.columns ?? tableColumns),
|
|
289
325
|
};
|
|
326
|
+
return index.expression !== undefined
|
|
327
|
+
? { ...base, expression: index.expression }
|
|
328
|
+
: { ...base, columns: index.columns ?? [] };
|
|
290
329
|
}
|
|
291
330
|
|
|
292
331
|
/**
|
|
@@ -399,7 +438,7 @@ function convertTable(
|
|
|
399
438
|
// the object→own-column `dependsOn` edge like any other index.
|
|
400
439
|
foreignKeys: table.foreignKeys.map((fk) => convertForeignKey(fk, storage)),
|
|
401
440
|
uniques: table.uniques.map((u) => convertUnique(u, name)),
|
|
402
|
-
indexes: table.indexes.map((i) => convertIndex(i, name)),
|
|
441
|
+
indexes: table.indexes.map((i) => convertIndex(i, name, Object.keys(table.columns))),
|
|
403
442
|
...ifDefined('checks', checks),
|
|
404
443
|
});
|
|
405
444
|
}
|
|
@@ -511,7 +550,15 @@ export function contractNamespaceToSchemaIR(
|
|
|
511
550
|
options: ContractToSchemaIROptions,
|
|
512
551
|
): SqlSchemaIR {
|
|
513
552
|
if (options.annotationNamespace.length === 0) {
|
|
514
|
-
throw
|
|
553
|
+
throw sqlFamilyError(
|
|
554
|
+
'CONTRACT.PACK_CONTRIBUTION_INVALID',
|
|
555
|
+
'annotationNamespace must be a non-empty string',
|
|
556
|
+
{
|
|
557
|
+
why: 'The calling target pack passed an empty annotationNamespace to the contract-to-schema-IR projection.',
|
|
558
|
+
fix: 'Fix the target pack to pass its non-empty annotation namespace (e.g. "pg").',
|
|
559
|
+
meta: { option: 'annotationNamespace' },
|
|
560
|
+
},
|
|
561
|
+
);
|
|
515
562
|
}
|
|
516
563
|
const namespace = storage.namespaces[namespaceId];
|
|
517
564
|
if (!namespace) {
|
|
@@ -539,7 +586,15 @@ export function contractToSchemaIR(
|
|
|
539
586
|
options: ContractToSchemaIROptions,
|
|
540
587
|
): SqlSchemaIR {
|
|
541
588
|
if (options.annotationNamespace.length === 0) {
|
|
542
|
-
throw
|
|
589
|
+
throw sqlFamilyError(
|
|
590
|
+
'CONTRACT.PACK_CONTRIBUTION_INVALID',
|
|
591
|
+
'annotationNamespace must be a non-empty string',
|
|
592
|
+
{
|
|
593
|
+
why: 'The calling target pack passed an empty annotationNamespace to the contract-to-schema-IR projection.',
|
|
594
|
+
fix: 'Fix the target pack to pass its non-empty annotation namespace (e.g. "pg").',
|
|
595
|
+
meta: { option: 'annotationNamespace' },
|
|
596
|
+
},
|
|
597
|
+
);
|
|
543
598
|
}
|
|
544
599
|
|
|
545
600
|
if (!contract) {
|
|
@@ -554,8 +609,14 @@ export function contractToSchemaIR(
|
|
|
554
609
|
StorageTable.assert(tableDefRaw, `namespaces.${ns.id}.entries.table.${tableName}`);
|
|
555
610
|
const tableDef = tableDefRaw;
|
|
556
611
|
if (tables[tableName] !== undefined) {
|
|
557
|
-
throw
|
|
612
|
+
throw sqlFamilyError(
|
|
613
|
+
'CONTRACT.TABLE_AMBIGUOUS',
|
|
558
614
|
`contractToSchemaIR: duplicate SQL table name "${tableName}" across namespaces (ambiguous for flat SqlSchemaIR.tables).`,
|
|
615
|
+
{
|
|
616
|
+
why: 'Two namespaces declare a table with the same name, which is ambiguous for the flat schema-IR table map.',
|
|
617
|
+
fix: 'Rename one of the tables so every table name is unique across namespaces.',
|
|
618
|
+
meta: { table: tableName },
|
|
619
|
+
},
|
|
559
620
|
);
|
|
560
621
|
}
|
|
561
622
|
tables[tableName] = convertTable(
|
|
@@ -334,9 +334,9 @@ export interface SqlMigrationPlannerPlanOptions {
|
|
|
334
334
|
* or `null` for reconciliation flows that have no prior contract.
|
|
335
335
|
*
|
|
336
336
|
* Required at every call site so the structural fact "I have a prior
|
|
337
|
-
* contract / I don't" is visible in the type. `migration plan` reads
|
|
338
|
-
*
|
|
339
|
-
* the parsed value; `db update` / `db init` reconcile against the
|
|
337
|
+
* contract / I don't" is visible in the type. `migration plan` reads the
|
|
338
|
+
* predecessor's contract from the snapshot store by its storage hash and
|
|
339
|
+
* passes the parsed value; `db update` / `db init` reconcile against the
|
|
340
340
|
* live schema and pass `null`. Strategies that
|
|
341
341
|
* need from/to column-shape comparisons (unsafe type change, nullability
|
|
342
342
|
* tightening) use this to decide whether to emit `dataTransform`
|
|
@@ -346,6 +346,13 @@ export interface SqlMigrationPlannerPlanOptions {
|
|
|
346
346
|
* plan's `describe()` as `fromContract?.storage.storageHash ?? null`.
|
|
347
347
|
*/
|
|
348
348
|
readonly fromContract: Contract<SqlStorage> | null;
|
|
349
|
+
/**
|
|
350
|
+
* POSIX-relative path from the migration package dir to
|
|
351
|
+
* `migrations/snapshots`, e.g. `'../../snapshots'`. Threaded straight
|
|
352
|
+
* into the produced plan's `renderTypeScript()` metadata so the
|
|
353
|
+
* scaffold's contract-snapshot imports point at the deduplicated store.
|
|
354
|
+
*/
|
|
355
|
+
readonly snapshotsImportPath: string;
|
|
349
356
|
/**
|
|
350
357
|
* Active framework components participating in this composition.
|
|
351
358
|
* Each component is target-bound so SQL targets can dispatch
|
|
@@ -169,15 +169,3 @@ export function deriveBackRelationFieldName(childModelName: string, isOneToOne:
|
|
|
169
169
|
const base = childModelName.charAt(0).toLowerCase() + childModelName.slice(1);
|
|
170
170
|
return isOneToOne ? base : pluralize(base);
|
|
171
171
|
}
|
|
172
|
-
|
|
173
|
-
export function toNamedTypeName(columnName: string): string {
|
|
174
|
-
let name: string;
|
|
175
|
-
|
|
176
|
-
if (hasSeparators(columnName)) {
|
|
177
|
-
name = snakeToPascalCase(columnName);
|
|
178
|
-
} else {
|
|
179
|
-
name = columnName.charAt(0).toUpperCase() + columnName.slice(1);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return escapeIfNeeded(name);
|
|
183
|
-
}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import type { ColumnDefault } from '@prisma-next/contract/types';
|
|
2
2
|
import type { DefaultMappingOptions } from './default-mapping';
|
|
3
3
|
|
|
4
|
-
export type
|
|
4
|
+
export type PslTypeReference = {
|
|
5
5
|
readonly name: string;
|
|
6
6
|
readonly args?: readonly string[];
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export type PslTypeResolution =
|
|
10
10
|
| {
|
|
11
|
-
readonly pslType:
|
|
11
|
+
readonly pslType: PslTypeReference;
|
|
12
12
|
readonly nativeType: string;
|
|
13
13
|
readonly typeParams?: Record<string, unknown>;
|
|
14
|
-
readonly nativeTypeAttribute?: PslNativeTypeAttribute;
|
|
15
14
|
}
|
|
16
15
|
| {
|
|
17
16
|
readonly unsupported: true;
|
|
@@ -123,6 +123,16 @@ function detectOneToOne(fk: SqlForeignKeyIR, table: SqlTableIR): boolean {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
for (const index of table.indexes) {
|
|
127
|
+
if (!index.unique || index.partial || index.columns === undefined) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const indexCols = [...index.columns].sort();
|
|
131
|
+
if (indexCols.length === fkCols.length && indexCols.every((c, i) => c === fkCols[i])) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
return false;
|
|
127
137
|
}
|
|
128
138
|
|
|
@@ -170,7 +180,19 @@ export function buildChildRelationField(
|
|
|
170
180
|
const onDelete = fk.onDelete && fk.onDelete !== DEFAULT_ON_DELETE ? fk.onDelete : undefined;
|
|
171
181
|
const onUpdate = fk.onUpdate && fk.onUpdate !== DEFAULT_ON_UPDATE ? fk.onUpdate : undefined;
|
|
172
182
|
const index =
|
|
173
|
-
hostTable &&
|
|
183
|
+
hostTable &&
|
|
184
|
+
!isBackedByColumnKeys(
|
|
185
|
+
fk.columns,
|
|
186
|
+
// Only indexes the inferrer actually emits can back the FK in the
|
|
187
|
+
// emitted contract: partial indexes (and expression indexes, which
|
|
188
|
+
// carry no column tuple) are not emitted yet, so
|
|
189
|
+
// they must not suppress the explicit `index: false`.
|
|
190
|
+
backingIndexColumnKeys({
|
|
191
|
+
indexes: hostTable.indexes.filter((i) => i.where === undefined),
|
|
192
|
+
uniques: hostTable.uniques,
|
|
193
|
+
primaryKey: hostTable.primaryKey,
|
|
194
|
+
}),
|
|
195
|
+
)
|
|
174
196
|
? false
|
|
175
197
|
: undefined;
|
|
176
198
|
|
package/src/core/verify.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ContractMarkerRecord } from '@prisma-next/contract/types';
|
|
2
2
|
import { type } from 'arktype';
|
|
3
|
+
import { sqlFamilyError } from './errors';
|
|
3
4
|
|
|
4
5
|
const MetaSchema = type({ '[string]': 'unknown' });
|
|
5
6
|
|
|
@@ -80,7 +81,15 @@ export function parseContractMarkerRow(row: unknown): ContractMarkerRecord {
|
|
|
80
81
|
const result = ContractMarkerRowSchema(row);
|
|
81
82
|
if (result instanceof type.errors) {
|
|
82
83
|
const messages = result.map((p: { message: string }) => p.message).join('; ');
|
|
83
|
-
throw
|
|
84
|
+
throw sqlFamilyError(
|
|
85
|
+
'CONTRACT.MARKER_ROW_CORRUPT',
|
|
86
|
+
`Invalid contract marker row: ${messages}`,
|
|
87
|
+
{
|
|
88
|
+
why: 'The contract marker row read from the database does not match the expected marker shape.',
|
|
89
|
+
fix: 'Re-sign the database with `prisma-next db sign`, or repair the marker table.',
|
|
90
|
+
meta: { issues: messages },
|
|
91
|
+
},
|
|
92
|
+
);
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
const updatedAt = result.updated_at
|
package/src/exports/psl-infer.ts
CHANGED
|
@@ -22,13 +22,12 @@ export {
|
|
|
22
22
|
toEnumName,
|
|
23
23
|
toFieldName,
|
|
24
24
|
toModelName,
|
|
25
|
-
toNamedTypeName,
|
|
26
25
|
} from '../core/psl-contract-infer/name-transforms';
|
|
27
26
|
export type {
|
|
28
27
|
EnumInfo,
|
|
29
|
-
PslNativeTypeAttribute,
|
|
30
28
|
PslPrinterOptions,
|
|
31
29
|
PslTypeMap,
|
|
30
|
+
PslTypeReference,
|
|
32
31
|
PslTypeResolution,
|
|
33
32
|
RelationField,
|
|
34
33
|
} from '../core/psl-contract-infer/printer-config';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sql-contract-serializer-C75cfMSS.mjs","names":["isPlainRecord"],"sources":["../src/core/ir/sql-contract-serializer-base.ts","../src/core/ir/sql-contract-serializer.ts"],"sourcesContent":["import { ContractValidationError } from '@prisma-next/contract/contract-validation-error';\nimport { isPlainRecord } from '@prisma-next/contract/is-plain-record';\nimport type { Contract } from '@prisma-next/contract/types';\nimport type { ContractSerializer } from '@prisma-next/framework-components/control';\nimport {\n type AnyEntityKindDescriptor,\n hydrateNamespaceEntities,\n type Namespace,\n} from '@prisma-next/framework-components/ir';\nimport { sqlContractCanonicalizationHooks } from '@prisma-next/sql-contract/canonicalization-hooks';\nimport { composeSqlEntityKinds } from '@prisma-next/sql-contract/entity-kinds';\nimport {\n isMaterializedSqlNamespace,\n type SqlNamespaceInput,\n SqlStorage,\n type SqlStorageInput,\n type SqlStorageTypeEntry,\n} from '@prisma-next/sql-contract/types';\nimport {\n createSqlContractSchema,\n validateSqlContractFully,\n} from '@prisma-next/sql-contract/validators';\nimport { blindCast } from '@prisma-next/utils/casts';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport type { JsonObject, JsonValue } from '@prisma-next/utils/json';\nimport { type Type, type } from 'arktype';\n\nconst NamespaceRawSchema = type({\n id: 'string',\n 'kind?': 'string',\n entries: type({\n '+': 'ignore',\n }),\n});\n\nexport type SqlEntityHydrationFactory = (entry: unknown) => unknown;\n\n/**\n * SQL family `ContractSerializer` abstract base. Carries the SQL-shared\n * deserialization pipeline:\n *\n * 1. `parseSqlContractStructure` validates the on-disk JSON envelope\n * against the SQL contract arktype schema (`validateSqlContractFully`)\n * and returns the validated flat-data shape.\n * 2. `hydrateSqlStorage` walks the validated `storage` subtree and\n * constructs the family-shared SQL Contract IR class hierarchy\n * (`SqlStorage` -> `StorageTable` -> `StorageColumn` / `PrimaryKey`\n * / …). The rest of the contract envelope is JSON-clean primitive\n * data and passes through unchanged.\n * 3. `constructTargetContract` is the target-specific extension hook;\n * defaults to identity. Targets that need to attach target-only\n * fields (e.g. target-specific derived storage fields) override it.\n *\n * Default `serializeContract` is identity over the contract — concrete\n * SQL targets ship JSON-clean class instances, so the contract value\n * can be stringified directly. The non-enumerable family-level `kind`\n * discriminator on `SqlNode` instances stays out of the persisted\n * envelope automatically. Targets that need to canonicalize on the way\n * out (key ordering, dropping computed-only fields) override\n * `serializeContract` directly.\n */\nexport abstract class SqlContractSerializerBase<TContract extends Contract<SqlStorage>>\n implements ContractSerializer<TContract>\n{\n private readonly contractSchema: Type<unknown> | undefined;\n private readonly entryKinds: ReadonlyMap<string, AnyEntityKindDescriptor>;\n\n constructor(\n protected readonly entityHydrationRegistry: ReadonlyMap<\n string,\n SqlEntityHydrationFactory\n > = new Map(),\n packEntityKinds: readonly AnyEntityKindDescriptor[] = [],\n ) {\n this.entryKinds = composeSqlEntityKinds(packEntityKinds);\n this.contractSchema =\n packEntityKinds.length > 0 ? createSqlContractSchema(this.entryKinds) : undefined;\n }\n\n deserializeContract<T extends TContract = TContract>(json: unknown): T {\n const validated = this.parseSqlContractStructure(json);\n const hydrated = this.hydrateSqlStorage(validated);\n return this.constructTargetContract(hydrated) as T;\n }\n\n serializeContract(contract: TContract): JsonObject {\n return contract as unknown as JsonObject;\n }\n\n shouldPreserveEmpty = sqlContractCanonicalizationHooks.shouldPreserveEmpty;\n\n sortStorage = sqlContractCanonicalizationHooks.sortStorage;\n\n protected parseSqlContractStructure(json: unknown): Contract<SqlStorage> {\n return validateSqlContractFully<Contract<SqlStorage>>(\n json,\n this.contractSchema !== undefined ? { contractSchema: this.contractSchema } : undefined,\n );\n }\n\n protected hydrateSqlStorage(validated: Contract<SqlStorage>): Contract<SqlStorage> {\n const types = validated.storage.types;\n const hydratedTypes =\n types !== undefined\n ? Object.fromEntries(\n Object.entries(types).map(([name, entry]) => [\n name,\n this.hydrateStorageTypeEntry(entry),\n ]),\n )\n : undefined;\n\n const rawNamespaces = validated.storage.namespaces;\n if (rawNamespaces === undefined) {\n throw new ContractValidationError(\n 'Contract storage.namespaces is required after structural validation',\n 'structural',\n );\n }\n const hydratedNamespaces = this.hydrateSqlNamespaceMap(\n blindCast<\n Readonly<Record<string, Record<string, unknown>>>,\n 'parseSqlContractStructure validated raw JSON; namespace entries are plain objects, not SqlNamespace instances.'\n >(rawNamespaces),\n );\n\n return {\n ...validated,\n storage: new SqlStorage({\n storageHash: validated.storage.storageHash,\n ...ifDefined('types', hydratedTypes),\n namespaces: blindCast<\n SqlStorageInput['namespaces'],\n 'hydrateSqlNamespaceMap builds each namespace through the target serializer override, so every value is a SqlNamespace; the framework return type only promises the base Namespace.'\n >(hydratedNamespaces),\n }),\n };\n }\n\n protected hydrateSqlNamespaceMap(\n namespaces: Readonly<Record<string, Record<string, unknown>>>,\n ): Readonly<Record<string, Namespace>> {\n return Object.fromEntries(\n Object.entries(namespaces).map(([nsId, namespaceEntryRaw]) => {\n const namespaceHydrated = this.hydrateSqlNamespaceEntry(nsId, namespaceEntryRaw);\n if (!isMaterializedSqlNamespace(namespaceHydrated)) {\n throw new Error(\n `Target serializer bug: hydrateSqlNamespaceEntry for namespace \"${nsId}\" returned a non-NamespaceBase value. Override hydrateSqlNamespaceEntry to produce a target namespace concretion.`,\n );\n }\n return [nsId, namespaceHydrated];\n }),\n );\n }\n\n protected hydrateSqlNamespaceEntry(\n nsId: string,\n raw: Record<string, unknown>,\n ): Namespace | SqlNamespaceInput {\n const id = typeof raw['id'] === 'string' ? raw['id'] : nsId;\n const parsed = NamespaceRawSchema({ ...raw, id });\n if (parsed instanceof type.errors) {\n const messages = parsed.map((p: { message: string }) => p.message).join('; ');\n throw new ContractValidationError(`Namespace hydration failed: ${messages}`, 'structural');\n }\n const entriesRaw = parsed.entries;\n const rawEntriesMap = isPlainRecord(entriesRaw) ? entriesRaw : {};\n\n const entriesInput: Record<string, Readonly<Record<string, unknown>>> = {};\n for (const [key, innerMap] of Object.entries(rawEntriesMap)) {\n entriesInput[key] = isPlainRecord(innerMap) ? innerMap : Object.freeze({});\n }\n\n const entriesOutput = hydrateNamespaceEntities(entriesInput, this.entryKinds, 'fail', id);\n\n // Always ensure a 'table' key is present (may be empty).\n if (!Object.hasOwn(entriesOutput, 'table')) {\n entriesOutput['table'] = {};\n }\n\n return blindCast<\n SqlNamespaceInput,\n 'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the SqlNamespaceInput the target createNamespace consumes.'\n >({\n id,\n entries: entriesOutput,\n });\n }\n\n protected hydrateStorageTypeEntry(entry: SqlStorageTypeEntry): SqlStorageTypeEntry {\n if (typeof entry !== 'object' || entry === null) {\n return entry;\n }\n const kind = (entry as { kind?: unknown }).kind;\n if (typeof kind !== 'string') {\n return entry;\n }\n const factory = this.entityHydrationRegistry.get(kind);\n if (factory === undefined) {\n return entry;\n }\n return blindCast<\n SqlStorageTypeEntry,\n 'entity registry factory returns SqlStorageTypeEntry for storage.types entries'\n >(factory(entry));\n }\n\n protected constructTargetContract(hydrated: Contract<SqlStorage>): TContract {\n return hydrated as TContract;\n }\n\n /**\n * Serializes a namespace's `entries` dict by walking every enumerable\n * kind — no kind is named here, mirroring the generic hydrate walk in\n * `hydrateSqlNamespaceEntry` above. `table` is the SQL family's one\n * universal base kind (every namespace carries it), so it is always\n * emitted, even when empty; every other kind — target- or\n * pack-contributed — is emitted only when it holds at least one entry.\n * A kind carried non-enumerable on `entries` is excluded here for free,\n * since `Object.entries` honors enumerability.\n */\n protected serializeNamespaceEntries(\n entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>,\n ): Record<string, Record<string, JsonObject>> {\n const out: Record<string, Record<string, JsonObject>> = {\n table: this.serializeEntries(entries['table'] ?? {}),\n };\n for (const [kind, record] of Object.entries(entries)) {\n if (kind === 'table' || record == null || Object.keys(record).length === 0) {\n continue;\n }\n out[kind] = this.serializeEntries(record);\n }\n return out;\n }\n\n private serializeEntries(entries: Readonly<Record<string, unknown>>): Record<string, JsonObject> {\n const out: Record<string, JsonObject> = {};\n for (const [name, entry] of Object.entries(entries)) {\n out[name] = this.serializeJsonObject(entry);\n }\n return out;\n }\n\n protected serializeJsonObject(value: unknown): JsonObject {\n return blindCast<\n JsonObject,\n 'serializeJsonValue round-trips an IR node through JSON, yielding a JsonObject'\n >(this.serializeJsonValue(value));\n }\n\n private serializeJsonValue(value: unknown): JsonValue {\n return blindCast<JsonValue, 'JSON.parse(JSON.stringify(x)) yields a JsonValue'>(\n JSON.parse(JSON.stringify(value)),\n );\n }\n}\n","import type { Contract } from '@prisma-next/contract/types';\nimport type { SqlStorage } from '@prisma-next/sql-contract/types';\nimport { SqlContractSerializerBase } from './sql-contract-serializer-base';\n\n/**\n * Default SQL family `ContractSerializer` concretion. Inherits the\n * full SQL-shared deserialization pipeline (structural validation +\n * IR-class hydration) without pack-registered `storage.types`\n * hydration factories — targets that emit polymorphic JSON outside the\n * codec-typed envelope wire a target-specific subclass with a populated\n * registry (see Postgres). Family-level call sites instantiate this\n * default directly when no target serializer is supplied.\n *\n * Because this serializer has no target concretion, deserialization of\n * contracts that include namespace entries from JSON will throw unless\n * the caller provides pre-hydrated `NamespaceBase` instances. Production\n * paths always supply a target-specific serializer.\n */\nexport class SqlContractSerializer extends SqlContractSerializerBase<Contract<SqlStorage>> {\n constructor() {\n super(new Map());\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"verify-C-G0obRm.mjs","names":[],"sources":["../src/core/verify.ts"],"sourcesContent":["import type { ContractMarkerRecord } from '@prisma-next/contract/types';\nimport { type } from 'arktype';\n\nconst MetaSchema = type({ '[string]': 'unknown' });\n\nfunction parseMeta(meta: unknown): Record<string, unknown> {\n if (meta === null || meta === undefined) {\n return {};\n }\n\n let parsed: unknown;\n if (typeof meta === 'string') {\n try {\n parsed = JSON.parse(meta);\n } catch {\n return {};\n }\n } else {\n parsed = meta;\n }\n\n const result = MetaSchema(parsed);\n if (result instanceof type.errors) {\n return {};\n }\n\n return result as Record<string, unknown>;\n}\n\n/**\n * SQLite stores `contract_json` as TEXT, so the wire shape is a JSON string;\n * Postgres uses `jsonb` and returns an already-parsed value. Normalize both\n * here so `ContractMarkerRecord.contractJson` is always the structured form.\n */\nfunction parseContractJson(value: unknown): unknown {\n if (value === null || value === undefined) return null;\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n}\n\n/**\n * Wire shape of a `prisma_contract.marker` row as it comes out of a SQL\n * driver. Snake-cased to match the on-disk column names. Shared by every\n * SQL target's `readMarker` so each runner doesn't redeclare it inline.\n */\nexport type ContractMarkerRow = {\n core_hash: string;\n profile_hash: string;\n contract_json: unknown | null;\n canonical_version: number | null;\n updated_at: Date | string;\n app_tag: string | null;\n meta: unknown | null;\n // SQLite stores arrays as JSON-TEXT, so this is `string` on the wire from\n // a SQLite driver and `string[]` from a Postgres driver. Targets normalize\n // before passing to `parseContractMarkerRow`.\n invariants: unknown;\n};\n\nconst ContractMarkerRowSchema = type({\n core_hash: 'string',\n profile_hash: 'string',\n 'contract_json?': 'unknown | null',\n 'canonical_version?': 'number | null',\n 'updated_at?': 'Date | string',\n 'app_tag?': 'string | null',\n 'meta?': 'unknown | null',\n invariants: type('string').array(),\n});\n\n/**\n * Parses a contract marker row from database query result.\n * This is SQL-specific parsing logic (handles SQL row structure with snake_case columns).\n */\nexport function parseContractMarkerRow(row: unknown): ContractMarkerRecord {\n const result = ContractMarkerRowSchema(row);\n if (result instanceof type.errors) {\n const messages = result.map((p: { message: string }) => p.message).join('; ');\n throw new Error(`Invalid contract marker row: ${messages}`);\n }\n\n const updatedAt = result.updated_at\n ? result.updated_at instanceof Date\n ? result.updated_at\n : new Date(result.updated_at)\n : new Date();\n\n return {\n storageHash: result.core_hash,\n profileHash: result.profile_hash,\n contractJson: parseContractJson(result.contract_json),\n canonicalVersion: result.canonical_version ?? null,\n updatedAt,\n appTag: result.app_tag ?? null,\n meta: parseMeta(result.meta),\n invariants: result.invariants,\n };\n}\n\n/**\n * Collects supported codec type IDs from adapter and extension manifests.\n * Returns a sorted, unique array of type IDs that are declared in the manifests.\n * This enables coverage checks by comparing contract column types against supported types.\n *\n * Note: This extracts type IDs from manifest type imports, not from runtime codec registries.\n * The manifests declare which codec types are available, but the actual type IDs\n * are defined in the codec-types TypeScript modules that are imported.\n *\n * For MVP, we return an empty array since extracting type IDs from TypeScript modules\n * would require runtime evaluation or static analysis. This can be enhanced later.\n */\nexport function collectSupportedCodecTypeIds(\n descriptors: ReadonlyArray<{ readonly id: string }>,\n): readonly string[] {\n // For MVP, return empty array\n // Future enhancement: Extract type IDs from codec-types modules via static analysis\n // or require manifests to explicitly list supported type IDs\n void descriptors;\n return [];\n}\n"],"mappings":";;AAGA,MAAM,aAAa,KAAK,EAAE,YAAY,UAAU,CAAC;AAEjD,SAAS,UAAU,MAAwC;CACzD,IAAI,SAAS,QAAQ,SAAS,KAAA,GAC5B,OAAO,CAAC;CAGV,IAAI;CACJ,IAAI,OAAO,SAAS,UAClB,IAAI;EACF,SAAS,KAAK,MAAM,IAAI;CAC1B,QAAQ;EACN,OAAO,CAAC;CACV;MAEA,SAAS;CAGX,MAAM,SAAS,WAAW,MAAM;CAChC,IAAI,kBAAkB,KAAK,QACzB,OAAO,CAAC;CAGV,OAAO;AACT;;;;;;AAOA,SAAS,kBAAkB,OAAyB;CAClD,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI;EACF,OAAO,KAAK,MAAM,KAAK;CACzB,QAAQ;EACN,OAAO;CACT;AACF;AAqBA,MAAM,0BAA0B,KAAK;CACnC,WAAW;CACX,cAAc;CACd,kBAAkB;CAClB,sBAAsB;CACtB,eAAe;CACf,YAAY;CACZ,SAAS;CACT,YAAY,KAAK,QAAQ,CAAC,CAAC,MAAM;AACnC,CAAC;;;;;AAMD,SAAgB,uBAAuB,KAAoC;CACzE,MAAM,SAAS,wBAAwB,GAAG;CAC1C,IAAI,kBAAkB,KAAK,QAAQ;EACjC,MAAM,WAAW,OAAO,KAAK,MAA2B,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI;EAC5E,MAAM,IAAI,MAAM,gCAAgC,UAAU;CAC5D;CAEA,MAAM,YAAY,OAAO,aACrB,OAAO,sBAAsB,OAC3B,OAAO,aACP,IAAI,KAAK,OAAO,UAAU,oBAC5B,IAAI,KAAK;CAEb,OAAO;EACL,aAAa,OAAO;EACpB,aAAa,OAAO;EACpB,cAAc,kBAAkB,OAAO,aAAa;EACpD,kBAAkB,OAAO,qBAAqB;EAC9C;EACA,QAAQ,OAAO,WAAW;EAC1B,MAAM,UAAU,OAAO,IAAI;EAC3B,YAAY,OAAO;CACrB;AACF;;;;;;;;;;;;;AAcA,SAAgB,6BACd,aACmB;CAKnB,OAAO,CAAC;AACV"}
|