@prisma-next/family-sql 0.14.0 → 0.15.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/dist/{authoring-type-constructors-CjFfO6LM.mjs → authoring-type-constructors-CXd-8ydc.mjs} +7 -30
- package/dist/authoring-type-constructors-CXd-8ydc.mjs.map +1 -0
- package/dist/{control-adapter-Cmw9LvEP.d.mts → control-adapter-BQgad8Zc.d.mts} +30 -8
- package/dist/control-adapter-BQgad8Zc.d.mts.map +1 -0
- package/dist/control-adapter.d.mts +1 -1
- package/dist/control.d.mts +279 -34
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +364 -1005
- package/dist/control.mjs.map +1 -1
- package/dist/diff.d.mts +102 -0
- package/dist/diff.d.mts.map +1 -0
- package/dist/diff.mjs +14 -0
- package/dist/diff.mjs.map +1 -0
- package/dist/ir.d.mts +26 -9
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +2 -2
- package/dist/ir.mjs.map +1 -1
- package/dist/migration.d.mts +11 -2
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +7 -0
- package/dist/migration.mjs.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/psl-infer.d.mts +119 -0
- package/dist/psl-infer.d.mts.map +1 -0
- package/dist/psl-infer.mjs +366 -0
- package/dist/psl-infer.mjs.map +1 -0
- package/dist/schema-differ-DnoopSXm.d.mts +45 -0
- package/dist/schema-differ-DnoopSXm.d.mts.map +1 -0
- package/dist/schema-verify-W3r631Jh.mjs +226 -0
- package/dist/schema-verify-W3r631Jh.mjs.map +1 -0
- package/dist/{sql-contract-serializer-BR2vC7Z-.mjs → sql-contract-serializer-C75cfMSS.mjs} +46 -20
- package/dist/sql-contract-serializer-C75cfMSS.mjs.map +1 -0
- package/dist/{types-kgstZ_Zd.d.mts → types-BPv_y7iS.d.mts} +36 -142
- package/dist/types-BPv_y7iS.d.mts.map +1 -0
- package/package.json +26 -26
- package/src/core/authoring-entity-types.ts +12 -37
- package/src/core/control-adapter.ts +11 -5
- package/src/core/control-instance.ts +212 -42
- package/src/core/control-target-descriptor.ts +98 -0
- package/src/core/diff/schema-verify.ts +324 -0
- package/src/core/diff/sql-schema-diff.ts +41 -0
- package/src/core/diff/verifier-disposition.ts +30 -0
- package/src/core/ir/sql-contract-serializer-base.ts +70 -56
- package/src/core/ir/sql-contract-serializer.ts +5 -7
- package/src/core/ir/sql-schema-verifier-base.ts +5 -5
- package/src/core/migrations/contract-to-schema-ir.ts +137 -51
- package/src/core/migrations/control-policy.ts +66 -115
- package/src/core/migrations/field-event-planner.ts +2 -2
- package/src/core/migrations/native-type-expander.ts +28 -0
- package/src/core/migrations/schema-differ.ts +41 -0
- package/src/core/migrations/types.ts +33 -37
- package/src/core/psl-contract-infer/name-transforms.ts +15 -0
- package/src/core/psl-contract-infer/printer-config.ts +14 -6
- package/src/core/psl-contract-infer/relation-inference.ts +30 -1
- package/src/core/sql-migration.ts +12 -1
- package/src/exports/control.ts +12 -2
- package/src/exports/diff.ts +25 -0
- package/src/exports/psl-infer.ts +40 -0
- package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +0 -1
- package/dist/control-adapter-Cmw9LvEP.d.mts.map +0 -1
- package/dist/schema-verify.d.mts +0 -39
- package/dist/schema-verify.d.mts.map +0 -1
- package/dist/schema-verify.mjs +0 -2
- package/dist/sql-contract-serializer-BR2vC7Z-.mjs.map +0 -1
- package/dist/test-utils.d.mts +0 -2
- package/dist/test-utils.mjs +0 -2
- package/dist/types-kgstZ_Zd.d.mts.map +0 -1
- package/dist/verify-sql-schema-thU-jKpf.d.mts +0 -66
- package/dist/verify-sql-schema-thU-jKpf.d.mts.map +0 -1
- package/dist/verify-sql-schema-xT4udQLQ.mjs +0 -1501
- package/dist/verify-sql-schema-xT4udQLQ.mjs.map +0 -1
- package/src/core/psl-contract-infer/postgres-default-mapping.ts +0 -16
- package/src/core/psl-contract-infer/postgres-type-map.ts +0 -157
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +0 -795
- package/src/core/schema-verify/control-verify-emit.ts +0 -46
- package/src/core/schema-verify/verifier-disposition.ts +0 -58
- package/src/core/schema-verify/verify-helpers.ts +0 -820
- package/src/core/schema-verify/verify-sql-schema.ts +0 -1311
- package/src/exports/schema-verify.ts +0 -18
- package/src/exports/test-utils.ts +0 -9
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { blindCast } from "@prisma-next/utils/casts";
|
|
2
|
+
import { assertUniqueCodecOwner, dispositionForCategory } from "@prisma-next/framework-components/control";
|
|
3
|
+
import { ifDefined } from "@prisma-next/utils/defined";
|
|
4
|
+
import { effectiveControlPolicy } from "@prisma-next/contract/types";
|
|
5
|
+
import { isStorageTypeInstance } from "@prisma-next/sql-contract/types";
|
|
6
|
+
import { RelationalSchemaNodeKind } from "@prisma-next/sql-schema-ir/types";
|
|
7
|
+
//#region src/core/assembly.ts
|
|
8
|
+
function hasCodecControlHooks(descriptor) {
|
|
9
|
+
if (typeof descriptor !== "object" || descriptor === null) return false;
|
|
10
|
+
const hooks = descriptor.types?.codecTypes?.controlPlaneHooks;
|
|
11
|
+
return hooks !== null && hooks !== void 0 && typeof hooks === "object";
|
|
12
|
+
}
|
|
13
|
+
function extractCodecControlHooks(descriptors) {
|
|
14
|
+
const hooks = /* @__PURE__ */ new Map();
|
|
15
|
+
const owners = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const descriptor of descriptors) {
|
|
17
|
+
if (typeof descriptor !== "object" || descriptor === null) continue;
|
|
18
|
+
if (!hasCodecControlHooks(descriptor)) continue;
|
|
19
|
+
const controlPlaneHooks = descriptor.types.codecTypes.controlPlaneHooks;
|
|
20
|
+
for (const [codecId, hook] of Object.entries(controlPlaneHooks)) {
|
|
21
|
+
assertUniqueCodecOwner({
|
|
22
|
+
codecId,
|
|
23
|
+
owners,
|
|
24
|
+
descriptorId: descriptor.id,
|
|
25
|
+
entityLabel: "control hooks",
|
|
26
|
+
entityOwnershipLabel: "owner"
|
|
27
|
+
});
|
|
28
|
+
hooks.set(codecId, hook);
|
|
29
|
+
owners.set(codecId, descriptor.id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return hooks;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/core/diff/verifier-disposition.ts
|
|
36
|
+
/**
|
|
37
|
+
* Classifies a codec `verifyType` hook finding into the target-neutral
|
|
38
|
+
* categories the framework grades. A storage type is a named type instance
|
|
39
|
+
* (e.g. a native enum); the only shape divergence it can carry is a change
|
|
40
|
+
* to its value set, so a paired mismatch always classifies as `valueDrift`.
|
|
41
|
+
*/
|
|
42
|
+
function classifyStorageTypeDiffIssue(issue) {
|
|
43
|
+
if (issue.reason === "not-found") return "declaredMissing";
|
|
44
|
+
if (issue.reason === "not-expected") return "extraAuxiliary";
|
|
45
|
+
return "valueDrift";
|
|
46
|
+
}
|
|
47
|
+
function verifierDisposition(controlPolicy, issue) {
|
|
48
|
+
return dispositionForCategory(controlPolicy, classifyStorageTypeDiffIssue(issue));
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/core/diff/schema-verify.ts
|
|
52
|
+
function issueNode(issue) {
|
|
53
|
+
const node = issue.expected ?? issue.actual;
|
|
54
|
+
if (node === void 0) return void 0;
|
|
55
|
+
return blindCast(node);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolves an issue's framework-neutral {@link DiffSubjectGranularity} on
|
|
59
|
+
* demand, from the issue's node's `nodeKind` via the target-provided
|
|
60
|
+
* `granularityOf` map. The node carries only its `nodeKind` identity, never a
|
|
61
|
+
* classification, and nothing is stamped onto the issue — every consumer
|
|
62
|
+
* (the family verdict below, the framework aggregate's unclaimed-elements
|
|
63
|
+
* sweep via {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable})
|
|
64
|
+
* calls this the same way, resolved by the family/target that owns the node
|
|
65
|
+
* vocabulary. `undefined` for an issue with no node.
|
|
66
|
+
*/
|
|
67
|
+
function classifyDiffSubjectGranularity(issue, granularityOf) {
|
|
68
|
+
const node = issueNode(issue);
|
|
69
|
+
return node === void 0 ? void 0 : granularityOf(node.nodeKind);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolves an issue's storage `entityKind` on demand, from the issue's
|
|
73
|
+
* node's `nodeKind` via the target-provided `entityKindOf` map — the sibling
|
|
74
|
+
* of {@link classifyDiffSubjectGranularity}, called the same way by the same
|
|
75
|
+
* consumers (via
|
|
76
|
+
* {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable}).
|
|
77
|
+
* `undefined` for an issue with no node, or for a node kind with no storage
|
|
78
|
+
* entity of its own.
|
|
79
|
+
*/
|
|
80
|
+
function classifyDiffEntityKind(issue, entityKindOf) {
|
|
81
|
+
const node = issueNode(issue);
|
|
82
|
+
return node === void 0 ? void 0 : entityKindOf(node.nodeKind);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Re-keys the legacy `classifySqlVerifierIssueKind` category mapping on the
|
|
86
|
+
* issue's {@link DiffSubjectGranularity} (resolved via `granularityOf`) + the
|
|
87
|
+
* issue reason. The vocabulary maps one-to-one: an undeclared live entity or
|
|
88
|
+
* namespace is `extraTopLevelObject`, an undeclared live field
|
|
89
|
+
* `extraNestedElement`, undeclared auxiliaries (constraints, indexes,
|
|
90
|
+
* defaults) and structural leaves (policies) `extraAuxiliary`; a value-set
|
|
91
|
+
* drift on a check node is `valueDrift`; every other paired divergence is
|
|
92
|
+
* `declaredIncompatible`; anything the database lacks is `declaredMissing`.
|
|
93
|
+
* `granularityOf` is the target's classifier, so target and extension node
|
|
94
|
+
* kinds classify without the family importing them.
|
|
95
|
+
*/
|
|
96
|
+
function classifySqlDiffIssue(issue, granularityOf) {
|
|
97
|
+
if (issue.reason === "not-found") return "declaredMissing";
|
|
98
|
+
if (issue.reason === "not-expected") {
|
|
99
|
+
const granularity = classifyDiffSubjectGranularity(issue, granularityOf);
|
|
100
|
+
if (granularity === "entity" || granularity === "namespace") return "extraTopLevelObject";
|
|
101
|
+
if (granularity === "field") return "extraNestedElement";
|
|
102
|
+
return "extraAuxiliary";
|
|
103
|
+
}
|
|
104
|
+
if (issueNode(issue)?.nodeKind === RelationalSchemaNodeKind.check) return "valueDrift";
|
|
105
|
+
return "declaredIncompatible";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Whether a `not-expected` issue is a strict-mode-only finding. The legacy
|
|
109
|
+
* walk detected every relational extra (namespaces, entities, fields, and
|
|
110
|
+
* their auxiliaries) only under `--strict`; the structural diff (roots, RLS
|
|
111
|
+
* policies, roles) was never strict-gated — its extras fail in both modes.
|
|
112
|
+
* Keyed on the issue's granularity, resolved via `granularityOf`.
|
|
113
|
+
*/
|
|
114
|
+
function isStrictOnlyExtra(issue, granularityOf) {
|
|
115
|
+
const granularity = classifyDiffSubjectGranularity(issue, granularityOf);
|
|
116
|
+
return granularity === "namespace" || granularity === "entity" || granularity === "field" || granularity === "auxiliary";
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Applies the two consumer filters to a diff issue list: strict gating
|
|
120
|
+
* (relational `not-expected` findings drop in lenient mode) and
|
|
121
|
+
* control-policy disposition (each surviving issue grades against its
|
|
122
|
+
* subject table's effective policy; suppressed issues drop, `observed`
|
|
123
|
+
* subjects warn). The verify verdict is `failures.length === 0`.
|
|
124
|
+
*/
|
|
125
|
+
function computeSqlDiffVerdict(input) {
|
|
126
|
+
const failures = [];
|
|
127
|
+
const warnings = [];
|
|
128
|
+
for (const issue of input.issues) {
|
|
129
|
+
if (!input.strict && issue.reason === "not-expected" && isStrictOnlyExtra(issue, input.granularityOf)) continue;
|
|
130
|
+
const disposition = dispositionForCategory(effectiveControlPolicy(input.resolveControlPolicy(issue), input.defaultControlPolicy), classifySqlDiffIssue(issue, input.granularityOf));
|
|
131
|
+
if (disposition === "suppress") continue;
|
|
132
|
+
if (disposition === "warn") {
|
|
133
|
+
warnings.push(issue);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
failures.push(issue);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
failures,
|
|
140
|
+
warnings
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function computeStorageTypeVerdict(input) {
|
|
144
|
+
const failures = [];
|
|
145
|
+
const warnings = [];
|
|
146
|
+
const policy = effectiveControlPolicy(void 0, input.contract.defaultControlPolicy);
|
|
147
|
+
for (const pair of input.namespacePairs) {
|
|
148
|
+
if (pair.actual === void 0) continue;
|
|
149
|
+
for (const [typeName, typeInstance] of Object.entries(input.contract.storage.types ?? {})) {
|
|
150
|
+
if (!isStorageTypeInstance(typeInstance)) continue;
|
|
151
|
+
const hook = input.codecHooks.get(typeInstance.codecId);
|
|
152
|
+
if (!hook?.verifyType) continue;
|
|
153
|
+
const typeIssues = hook.verifyType({
|
|
154
|
+
typeName,
|
|
155
|
+
typeInstance,
|
|
156
|
+
schema: pair.actual
|
|
157
|
+
});
|
|
158
|
+
for (const issue of typeIssues) {
|
|
159
|
+
const disposition = verifierDisposition(policy, issue);
|
|
160
|
+
if (disposition === "suppress") continue;
|
|
161
|
+
if (disposition === "warn") {
|
|
162
|
+
warnings.push(issue);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
failures.push(issue);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
failures,
|
|
171
|
+
warnings
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* THE SQL schema verify: runs the target's full-tree node diff, grades it
|
|
176
|
+
* through the family's post-diff filters (strict gating + control-policy
|
|
177
|
+
* disposition) plus the codec `verifyType` hook findings, and wraps the
|
|
178
|
+
* verdict in the issue-based result envelope. `ok` holds exactly when both
|
|
179
|
+
* issue lists are empty — the lists carry the verdict's failures.
|
|
180
|
+
*/
|
|
181
|
+
function verifySqlSchemaByDiff(input) {
|
|
182
|
+
const startTime = Date.now();
|
|
183
|
+
const verdictDiff = input.diffSchema({
|
|
184
|
+
contract: input.contract,
|
|
185
|
+
schema: input.schema,
|
|
186
|
+
frameworkComponents: input.frameworkComponents
|
|
187
|
+
});
|
|
188
|
+
const diffVerdict = computeSqlDiffVerdict({
|
|
189
|
+
issues: verdictDiff.issues,
|
|
190
|
+
resolveControlPolicy: verdictDiff.resolveControlPolicy,
|
|
191
|
+
strict: input.strict,
|
|
192
|
+
defaultControlPolicy: input.contract.defaultControlPolicy,
|
|
193
|
+
granularityOf: input.granularityOf
|
|
194
|
+
});
|
|
195
|
+
const storageTypeVerdict = computeStorageTypeVerdict({
|
|
196
|
+
contract: input.contract,
|
|
197
|
+
namespacePairs: verdictDiff.namespacePairs,
|
|
198
|
+
codecHooks: extractCodecControlHooks(input.frameworkComponents)
|
|
199
|
+
});
|
|
200
|
+
const failCount = diffVerdict.failures.length + storageTypeVerdict.failures.length;
|
|
201
|
+
const ok = failCount === 0;
|
|
202
|
+
const profileHash = "profileHash" in input.contract && typeof input.contract.profileHash === "string" ? input.contract.profileHash : void 0;
|
|
203
|
+
return {
|
|
204
|
+
ok,
|
|
205
|
+
...ok ? {} : { code: "PN-SCHEMA-0001" },
|
|
206
|
+
summary: ok ? "Database schema satisfies contract" : `Database schema does not satisfy contract (${failCount} failure${failCount === 1 ? "" : "s"})`,
|
|
207
|
+
contract: {
|
|
208
|
+
storageHash: input.contract.storage.storageHash,
|
|
209
|
+
...ifDefined("profileHash", profileHash)
|
|
210
|
+
},
|
|
211
|
+
target: {
|
|
212
|
+
expected: input.contract.target,
|
|
213
|
+
actual: input.contract.target
|
|
214
|
+
},
|
|
215
|
+
schema: {
|
|
216
|
+
issues: [...diffVerdict.failures, ...storageTypeVerdict.failures],
|
|
217
|
+
warnings: { issues: [...diffVerdict.warnings, ...storageTypeVerdict.warnings] }
|
|
218
|
+
},
|
|
219
|
+
meta: { strict: input.strict },
|
|
220
|
+
timings: { total: Date.now() - startTime }
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
export { computeStorageTypeVerdict as a, computeSqlDiffVerdict as i, classifyDiffSubjectGranularity as n, verifySqlSchemaByDiff as o, classifySqlDiffIssue as r, extractCodecControlHooks as s, classifyDiffEntityKind as t };
|
|
225
|
+
|
|
226
|
+
//# sourceMappingURL=schema-verify-W3r631Jh.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-verify-W3r631Jh.mjs","names":["d"],"sources":["../src/core/assembly.ts","../src/core/diff/verifier-disposition.ts","../src/core/diff/schema-verify.ts"],"sourcesContent":["import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';\nimport { assertUniqueCodecOwner } from '@prisma-next/framework-components/control';\nimport type { CodecControlHooks } from './migrations/types';\n\ntype CodecControlHooksMap = Record<string, CodecControlHooks>;\n\nfunction hasCodecControlHooks(descriptor: unknown): descriptor is {\n readonly id: string;\n readonly types: {\n readonly codecTypes: {\n readonly controlPlaneHooks: CodecControlHooksMap;\n };\n };\n} {\n if (typeof descriptor !== 'object' || descriptor === null) {\n return false;\n }\n const d = descriptor as { types?: { codecTypes?: { controlPlaneHooks?: unknown } } };\n const hooks = d.types?.codecTypes?.controlPlaneHooks;\n return hooks !== null && hooks !== undefined && typeof hooks === 'object';\n}\n\nexport function extractCodecControlHooks(\n descriptors: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>,\n): Map<string, CodecControlHooks> {\n const hooks = new Map<string, CodecControlHooks>();\n const owners = new Map<string, string>();\n\n for (const descriptor of descriptors) {\n if (typeof descriptor !== 'object' || descriptor === null) {\n continue;\n }\n if (!hasCodecControlHooks(descriptor)) {\n continue;\n }\n const controlPlaneHooks = descriptor.types.codecTypes.controlPlaneHooks;\n for (const [codecId, hook] of Object.entries(controlPlaneHooks)) {\n assertUniqueCodecOwner({\n codecId,\n owners,\n descriptorId: descriptor.id,\n entityLabel: 'control hooks',\n entityOwnershipLabel: 'owner',\n });\n hooks.set(codecId, hook);\n owners.set(codecId, descriptor.id);\n }\n }\n\n return hooks;\n}\n","import type { ControlPolicy } from '@prisma-next/contract/types';\nimport type {\n SchemaDiffIssue,\n VerifierIssueCategory,\n VerifierOutcome,\n} from '@prisma-next/framework-components/control';\nimport { dispositionForCategory } from '@prisma-next/framework-components/control';\n\n/**\n * Classifies a codec `verifyType` hook finding into the target-neutral\n * categories the framework grades. A storage type is a named type instance\n * (e.g. a native enum); the only shape divergence it can carry is a change\n * to its value set, so a paired mismatch always classifies as `valueDrift`.\n */\nexport function classifyStorageTypeDiffIssue(issue: SchemaDiffIssue): VerifierIssueCategory {\n if (issue.reason === 'not-found') {\n return 'declaredMissing';\n }\n if (issue.reason === 'not-expected') {\n return 'extraAuxiliary';\n }\n return 'valueDrift';\n}\n\nexport function verifierDisposition(\n controlPolicy: ControlPolicy,\n issue: SchemaDiffIssue,\n): VerifierOutcome {\n return dispositionForCategory(controlPolicy, classifyStorageTypeDiffIssue(issue));\n}\n","/**\n * The differ-based SQL schema verify: post-diff filters and verdict.\n *\n * The generic node differ (`diffSchemas`) reports every node-level\n * difference between the derived expected tree and the introspected actual\n * tree. This module is the consumer side the spec assigns to the SQL\n * family: strict-mode extras gating and control-policy disposition are\n * reason/kind-keyed filters applied AFTER the diff — never inside it — and\n * the verify verdict derives from the filtered issue list.\n *\n * `verifySqlSchemaByDiff` wraps the verdict in the issue-based result\n * envelope — this is THE SQL schema verify (the legacy relational walk and\n * its verification tree are retired).\n */\n\nimport type { Contract, ControlPolicy } from '@prisma-next/contract/types';\nimport { effectiveControlPolicy } from '@prisma-next/contract/types';\nimport type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';\nimport type {\n DiffSubjectGranularity,\n SchemaDiffIssue,\n VerifierIssueCategory,\n VerifierOutcome,\n VerifyDatabaseSchemaResult,\n} from '@prisma-next/framework-components/control';\nimport { dispositionForCategory } from '@prisma-next/framework-components/control';\nimport { isStorageTypeInstance, type SqlStorage } from '@prisma-next/sql-contract/types';\nimport { RelationalSchemaNodeKind, type SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';\nimport { blindCast } from '@prisma-next/utils/casts';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { extractCodecControlHooks } from '../assembly';\nimport type { SqlSchemaDiffFn } from '../migrations/schema-differ';\nimport type { CodecControlHooks } from '../migrations/types';\nimport { verifierDisposition } from './verifier-disposition';\n\n// ============================================================================\n// Subject-granularity classification — nodeKind → framework-neutral granularity\n// ============================================================================\n\nfunction issueNode(issue: SchemaDiffIssue): SqlSchemaIRNode | undefined {\n const node = issue.expected ?? issue.actual;\n if (node === undefined) return undefined;\n return blindCast<\n SqlSchemaIRNode,\n 'every node in a SQL schema diff tree is a SqlSchemaIRNode; nodeKind is its identity'\n >(node);\n}\n\n/**\n * Resolves an issue's framework-neutral {@link DiffSubjectGranularity} on\n * demand, from the issue's node's `nodeKind` via the target-provided\n * `granularityOf` map. The node carries only its `nodeKind` identity, never a\n * classification, and nothing is stamped onto the issue — every consumer\n * (the family verdict below, the framework aggregate's unclaimed-elements\n * sweep via {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable})\n * calls this the same way, resolved by the family/target that owns the node\n * vocabulary. `undefined` for an issue with no node.\n */\nexport function classifyDiffSubjectGranularity(\n issue: SchemaDiffIssue,\n granularityOf: (nodeKind: string) => DiffSubjectGranularity,\n): DiffSubjectGranularity | undefined {\n const node = issueNode(issue);\n return node === undefined ? undefined : granularityOf(node.nodeKind);\n}\n\n/**\n * Resolves an issue's storage `entityKind` on demand, from the issue's\n * node's `nodeKind` via the target-provided `entityKindOf` map — the sibling\n * of {@link classifyDiffSubjectGranularity}, called the same way by the same\n * consumers (via\n * {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable}).\n * `undefined` for an issue with no node, or for a node kind with no storage\n * entity of its own.\n */\nexport function classifyDiffEntityKind(\n issue: SchemaDiffIssue,\n entityKindOf: (nodeKind: string) => string | undefined,\n): string | undefined {\n const node = issueNode(issue);\n return node === undefined ? undefined : entityKindOf(node.nodeKind);\n}\n\n// ============================================================================\n// Issue classification — subject granularity + reason → target-neutral category\n// ============================================================================\n\n/**\n * Re-keys the legacy `classifySqlVerifierIssueKind` category mapping on the\n * issue's {@link DiffSubjectGranularity} (resolved via `granularityOf`) + the\n * issue reason. The vocabulary maps one-to-one: an undeclared live entity or\n * namespace is `extraTopLevelObject`, an undeclared live field\n * `extraNestedElement`, undeclared auxiliaries (constraints, indexes,\n * defaults) and structural leaves (policies) `extraAuxiliary`; a value-set\n * drift on a check node is `valueDrift`; every other paired divergence is\n * `declaredIncompatible`; anything the database lacks is `declaredMissing`.\n * `granularityOf` is the target's classifier, so target and extension node\n * kinds classify without the family importing them.\n */\nexport function classifySqlDiffIssue(\n issue: SchemaDiffIssue,\n granularityOf: (nodeKind: string) => DiffSubjectGranularity,\n): VerifierIssueCategory {\n if (issue.reason === 'not-found') {\n return 'declaredMissing';\n }\n if (issue.reason === 'not-expected') {\n const granularity = classifyDiffSubjectGranularity(issue, granularityOf);\n if (granularity === 'entity' || granularity === 'namespace') {\n return 'extraTopLevelObject';\n }\n if (granularity === 'field') {\n return 'extraNestedElement';\n }\n return 'extraAuxiliary';\n }\n if (issueNode(issue)?.nodeKind === RelationalSchemaNodeKind.check) {\n return 'valueDrift';\n }\n return 'declaredIncompatible';\n}\n\n/**\n * Whether a `not-expected` issue is a strict-mode-only finding. The legacy\n * walk detected every relational extra (namespaces, entities, fields, and\n * their auxiliaries) only under `--strict`; the structural diff (roots, RLS\n * policies, roles) was never strict-gated — its extras fail in both modes.\n * Keyed on the issue's granularity, resolved via `granularityOf`.\n */\nfunction isStrictOnlyExtra(\n issue: SchemaDiffIssue,\n granularityOf: (nodeKind: string) => DiffSubjectGranularity,\n): boolean {\n const granularity = classifyDiffSubjectGranularity(issue, granularityOf);\n return (\n granularity === 'namespace' ||\n granularity === 'entity' ||\n granularity === 'field' ||\n granularity === 'auxiliary'\n );\n}\n\n// ============================================================================\n// The post-diff filter + verdict\n// ============================================================================\n\nexport interface SqlDiffVerdictInput {\n /** The full, ownership-scoped diff issue list from the target's differ. */\n readonly issues: readonly SchemaDiffIssue[];\n /** Resolves a diff issue's subject table's declared control policy directly from the contract. */\n readonly resolveControlPolicy: (issue: SchemaDiffIssue) => ControlPolicy | undefined;\n readonly strict: boolean;\n readonly defaultControlPolicy: ControlPolicy | undefined;\n /** The target's classifier: a diff issue node's `nodeKind` → its subject granularity. */\n readonly granularityOf: (nodeKind: string) => DiffSubjectGranularity;\n}\n\nexport interface SqlDiffVerdict {\n readonly failures: readonly SchemaDiffIssue[];\n readonly warnings: readonly SchemaDiffIssue[];\n}\n\n/**\n * Applies the two consumer filters to a diff issue list: strict gating\n * (relational `not-expected` findings drop in lenient mode) and\n * control-policy disposition (each surviving issue grades against its\n * subject table's effective policy; suppressed issues drop, `observed`\n * subjects warn). The verify verdict is `failures.length === 0`.\n */\nexport function computeSqlDiffVerdict(input: SqlDiffVerdictInput): SqlDiffVerdict {\n const failures: SchemaDiffIssue[] = [];\n const warnings: SchemaDiffIssue[] = [];\n for (const issue of input.issues) {\n if (\n !input.strict &&\n issue.reason === 'not-expected' &&\n isStrictOnlyExtra(issue, input.granularityOf)\n ) {\n continue;\n }\n const tablePolicy = input.resolveControlPolicy(issue);\n const policy = effectiveControlPolicy(tablePolicy, input.defaultControlPolicy);\n const disposition: VerifierOutcome = dispositionForCategory(\n policy,\n classifySqlDiffIssue(issue, input.granularityOf),\n );\n if (disposition === 'suppress') continue;\n if (disposition === 'warn') {\n warnings.push(issue);\n continue;\n }\n failures.push(issue);\n }\n return { failures, warnings };\n}\n\n// ============================================================================\n// Storage-types check — the codec verifyType hook path\n// ============================================================================\n\nexport interface StorageTypeVerdictInput {\n readonly contract: Contract<SqlStorage>;\n /**\n * Expected/actual namespace-node pairs the target's differ input produced:\n * for a namespaced tree, one entry per expected namespace with a\n * non-empty table set, paired by DDL schema name (absent actual side for\n * a schema the database lacks); a flat tree is the sole pair.\n */\n readonly namespacePairs: ReadonlyArray<{\n readonly actual: SqlSchemaIRNode | undefined;\n }>;\n readonly codecHooks: ReadonlyMap<string, CodecControlHooks>;\n}\n\n/**\n * Runs the codec `verifyType` hooks the way the legacy walk did: once per\n * contract namespace with tables, against that namespace's paired actual\n * node (the hook reads namespace-scoped state such as `enums` off it).\n * Issue dispositions grade against the contract default policy, matching\n * the legacy `pushTypeNode` semantics.\n */\nexport interface StorageTypeVerdict {\n readonly failures: readonly SchemaDiffIssue[];\n readonly warnings: readonly SchemaDiffIssue[];\n}\n\nexport function computeStorageTypeVerdict(input: StorageTypeVerdictInput): StorageTypeVerdict {\n const failures: SchemaDiffIssue[] = [];\n const warnings: SchemaDiffIssue[] = [];\n const policy = effectiveControlPolicy(undefined, input.contract.defaultControlPolicy);\n for (const pair of input.namespacePairs) {\n if (pair.actual === undefined) continue;\n for (const [typeName, typeInstance] of Object.entries(input.contract.storage.types ?? {})) {\n if (!isStorageTypeInstance(typeInstance)) continue;\n const hook = input.codecHooks.get(typeInstance.codecId);\n if (!hook?.verifyType) continue;\n const typeIssues = hook.verifyType({ typeName, typeInstance, schema: pair.actual });\n for (const issue of typeIssues) {\n const disposition = verifierDisposition(policy, issue);\n if (disposition === 'suppress') continue;\n if (disposition === 'warn') {\n warnings.push(issue);\n continue;\n }\n failures.push(issue);\n }\n }\n }\n return { failures, warnings };\n}\n\n// ============================================================================\n// The issue-based verify envelope\n// ============================================================================\n\nexport interface VerifySqlSchemaByDiffInput {\n readonly contract: Contract<SqlStorage>;\n readonly schema: SqlSchemaIRNode;\n readonly strict: boolean;\n readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;\n /** The target's full-tree node diff (`diffSchema` descriptor hook). */\n readonly diffSchema: SqlSchemaDiffFn;\n /** The target's classifier: a diff issue node's `nodeKind` → its subject granularity. */\n readonly granularityOf: (nodeKind: string) => DiffSubjectGranularity;\n}\n\n/**\n * THE SQL schema verify: runs the target's full-tree node diff, grades it\n * through the family's post-diff filters (strict gating + control-policy\n * disposition) plus the codec `verifyType` hook findings, and wraps the\n * verdict in the issue-based result envelope. `ok` holds exactly when both\n * issue lists are empty — the lists carry the verdict's failures.\n */\nexport function verifySqlSchemaByDiff(\n input: VerifySqlSchemaByDiffInput,\n): VerifyDatabaseSchemaResult {\n const startTime = Date.now();\n const verdictDiff = input.diffSchema({\n contract: input.contract,\n schema: input.schema,\n frameworkComponents: input.frameworkComponents,\n });\n const diffVerdict = computeSqlDiffVerdict({\n issues: verdictDiff.issues,\n resolveControlPolicy: verdictDiff.resolveControlPolicy,\n strict: input.strict,\n defaultControlPolicy: input.contract.defaultControlPolicy,\n granularityOf: input.granularityOf,\n });\n const storageTypeVerdict = computeStorageTypeVerdict({\n contract: input.contract,\n namespacePairs: verdictDiff.namespacePairs,\n codecHooks: extractCodecControlHooks(input.frameworkComponents),\n });\n const failCount = diffVerdict.failures.length + storageTypeVerdict.failures.length;\n const ok = failCount === 0;\n const profileHash =\n 'profileHash' in input.contract && typeof input.contract.profileHash === 'string'\n ? input.contract.profileHash\n : undefined;\n return {\n ok,\n ...(ok ? {} : { code: 'PN-SCHEMA-0001' }),\n summary: ok\n ? 'Database schema satisfies contract'\n : `Database schema does not satisfy contract (${failCount} failure${failCount === 1 ? '' : 's'})`,\n contract: {\n storageHash: input.contract.storage.storageHash,\n ...ifDefined('profileHash', profileHash),\n },\n target: {\n expected: input.contract.target,\n actual: input.contract.target,\n },\n schema: {\n issues: [...diffVerdict.failures, ...storageTypeVerdict.failures],\n warnings: {\n issues: [...diffVerdict.warnings, ...storageTypeVerdict.warnings],\n },\n },\n meta: { strict: input.strict },\n timings: { total: Date.now() - startTime },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { blindCast } from "@prisma-next/utils/casts";
|
|
2
|
+
import { hydrateNamespaceEntities } from "@prisma-next/framework-components/ir";
|
|
2
3
|
import { sqlContractCanonicalizationHooks } from "@prisma-next/sql-contract/canonicalization-hooks";
|
|
3
4
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
4
|
-
import {
|
|
5
|
+
import { SqlStorage, isMaterializedSqlNamespace } from "@prisma-next/sql-contract/types";
|
|
5
6
|
import { ContractValidationError } from "@prisma-next/contract/contract-validation-error";
|
|
6
|
-
import { isPlainRecord } from "@prisma-next/contract/is-plain-record";
|
|
7
|
+
import { isPlainRecord as isPlainRecord$1 } from "@prisma-next/contract/is-plain-record";
|
|
7
8
|
import { composeSqlEntityKinds } from "@prisma-next/sql-contract/entity-kinds";
|
|
8
|
-
import { SqlStorage, SqlUnboundNamespace, buildSqlNamespace } from "@prisma-next/sql-contract/types";
|
|
9
9
|
import { createSqlContractSchema, validateSqlContractFully } from "@prisma-next/sql-contract/validators";
|
|
10
10
|
import { type } from "arktype";
|
|
11
11
|
//#region src/core/ir/sql-contract-serializer-base.ts
|
|
@@ -65,39 +65,34 @@ var SqlContractSerializerBase = class {
|
|
|
65
65
|
const hydratedTypes = types !== void 0 ? Object.fromEntries(Object.entries(types).map(([name, entry]) => [name, this.hydrateStorageTypeEntry(entry)])) : void 0;
|
|
66
66
|
const rawNamespaces = validated.storage.namespaces;
|
|
67
67
|
if (rawNamespaces === void 0) throw new ContractValidationError("Contract storage.namespaces is required after structural validation", "structural");
|
|
68
|
-
const hydratedNamespaces = this.hydrateSqlNamespaceMap(rawNamespaces);
|
|
69
|
-
const withInjectedUnbound = this.defaultNamespaceId === UNBOUND_NAMESPACE_ID ? {
|
|
70
|
-
...hydratedNamespaces,
|
|
71
|
-
[UNBOUND_NAMESPACE_ID]: hydratedNamespaces[UNBOUND_NAMESPACE_ID] ?? SqlUnboundNamespace.instance
|
|
72
|
-
} : hydratedNamespaces;
|
|
68
|
+
const hydratedNamespaces = this.hydrateSqlNamespaceMap(blindCast(rawNamespaces));
|
|
73
69
|
return {
|
|
74
70
|
...validated,
|
|
75
71
|
storage: new SqlStorage({
|
|
76
72
|
storageHash: validated.storage.storageHash,
|
|
77
73
|
...ifDefined("types", hydratedTypes),
|
|
78
|
-
namespaces: blindCast(
|
|
74
|
+
namespaces: blindCast(hydratedNamespaces)
|
|
79
75
|
})
|
|
80
76
|
};
|
|
81
77
|
}
|
|
82
78
|
hydrateSqlNamespaceMap(namespaces) {
|
|
83
79
|
return Object.fromEntries(Object.entries(namespaces).map(([nsId, namespaceEntryRaw]) => {
|
|
84
80
|
const namespaceHydrated = this.hydrateSqlNamespaceEntry(nsId, namespaceEntryRaw);
|
|
85
|
-
|
|
81
|
+
if (!isMaterializedSqlNamespace(namespaceHydrated)) throw new Error(`Target serializer bug: hydrateSqlNamespaceEntry for namespace "${nsId}" returned a non-NamespaceBase value. Override hydrateSqlNamespaceEntry to produce a target namespace concretion.`);
|
|
82
|
+
return [nsId, namespaceHydrated];
|
|
86
83
|
}));
|
|
87
84
|
}
|
|
88
85
|
hydrateSqlNamespaceEntry(nsId, raw) {
|
|
89
|
-
|
|
90
|
-
const rawRecord = isPlainRecord(raw) ? raw : {};
|
|
91
|
-
const id = typeof rawRecord["id"] === "string" ? rawRecord["id"] : nsId;
|
|
86
|
+
const id = typeof raw["id"] === "string" ? raw["id"] : nsId;
|
|
92
87
|
const parsed = NamespaceRawSchema({
|
|
93
|
-
...
|
|
88
|
+
...raw,
|
|
94
89
|
id
|
|
95
90
|
});
|
|
96
91
|
if (parsed instanceof type.errors) throw new ContractValidationError(`Namespace hydration failed: ${parsed.map((p) => p.message).join("; ")}`, "structural");
|
|
97
92
|
const entriesRaw = parsed.entries;
|
|
98
|
-
const rawEntriesMap = isPlainRecord(entriesRaw) ? entriesRaw : {};
|
|
93
|
+
const rawEntriesMap = isPlainRecord$1(entriesRaw) ? entriesRaw : {};
|
|
99
94
|
const entriesInput = {};
|
|
100
|
-
for (const [key, innerMap] of Object.entries(rawEntriesMap)) entriesInput[key] = isPlainRecord(innerMap) ? innerMap : Object.freeze({});
|
|
95
|
+
for (const [key, innerMap] of Object.entries(rawEntriesMap)) entriesInput[key] = isPlainRecord$1(innerMap) ? innerMap : Object.freeze({});
|
|
101
96
|
const entriesOutput = hydrateNamespaceEntities(entriesInput, this.entryKinds, "fail", id);
|
|
102
97
|
if (!Object.hasOwn(entriesOutput, "table")) entriesOutput["table"] = {};
|
|
103
98
|
return blindCast({
|
|
@@ -116,6 +111,35 @@ var SqlContractSerializerBase = class {
|
|
|
116
111
|
constructTargetContract(hydrated) {
|
|
117
112
|
return hydrated;
|
|
118
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Serializes a namespace's `entries` dict by walking every enumerable
|
|
116
|
+
* kind — no kind is named here, mirroring the generic hydrate walk in
|
|
117
|
+
* `hydrateSqlNamespaceEntry` above. `table` is the SQL family's one
|
|
118
|
+
* universal base kind (every namespace carries it), so it is always
|
|
119
|
+
* emitted, even when empty; every other kind — target- or
|
|
120
|
+
* pack-contributed — is emitted only when it holds at least one entry.
|
|
121
|
+
* A kind carried non-enumerable on `entries` is excluded here for free,
|
|
122
|
+
* since `Object.entries` honors enumerability.
|
|
123
|
+
*/
|
|
124
|
+
serializeNamespaceEntries(entries) {
|
|
125
|
+
const out = { table: this.serializeEntries(entries["table"] ?? {}) };
|
|
126
|
+
for (const [kind, record] of Object.entries(entries)) {
|
|
127
|
+
if (kind === "table" || record == null || Object.keys(record).length === 0) continue;
|
|
128
|
+
out[kind] = this.serializeEntries(record);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
serializeEntries(entries) {
|
|
133
|
+
const out = {};
|
|
134
|
+
for (const [name, entry] of Object.entries(entries)) out[name] = this.serializeJsonObject(entry);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
serializeJsonObject(value) {
|
|
138
|
+
return blindCast(this.serializeJsonValue(value));
|
|
139
|
+
}
|
|
140
|
+
serializeJsonValue(value) {
|
|
141
|
+
return blindCast(JSON.parse(JSON.stringify(value)));
|
|
142
|
+
}
|
|
119
143
|
};
|
|
120
144
|
//#endregion
|
|
121
145
|
//#region src/core/ir/sql-contract-serializer.ts
|
|
@@ -127,16 +151,18 @@ var SqlContractSerializerBase = class {
|
|
|
127
151
|
* codec-typed envelope wire a target-specific subclass with a populated
|
|
128
152
|
* registry (see Postgres). Family-level call sites instantiate this
|
|
129
153
|
* default directly when no target serializer is supplied.
|
|
154
|
+
*
|
|
155
|
+
* Because this serializer has no target concretion, deserialization of
|
|
156
|
+
* contracts that include namespace entries from JSON will throw unless
|
|
157
|
+
* the caller provides pre-hydrated `NamespaceBase` instances. Production
|
|
158
|
+
* paths always supply a target-specific serializer.
|
|
130
159
|
*/
|
|
131
160
|
var SqlContractSerializer = class extends SqlContractSerializerBase {
|
|
132
161
|
constructor() {
|
|
133
162
|
super(/* @__PURE__ */ new Map());
|
|
134
163
|
}
|
|
135
|
-
get defaultNamespaceId() {
|
|
136
|
-
return UNBOUND_NAMESPACE_ID;
|
|
137
|
-
}
|
|
138
164
|
};
|
|
139
165
|
//#endregion
|
|
140
166
|
export { SqlContractSerializerBase as n, SqlContractSerializer as t };
|
|
141
167
|
|
|
142
|
-
//# sourceMappingURL=sql-contract-serializer-
|
|
168
|
+
//# sourceMappingURL=sql-contract-serializer-C75cfMSS.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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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"}
|