@prisma-next/sql-contract-ts 0.3.0-pr.99.6 → 0.3.0
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/LICENSE +201 -0
- package/README.md +206 -73
- package/dist/config-types.d.mts +8 -0
- package/dist/config-types.d.mts.map +1 -0
- package/dist/config-types.mjs +14 -0
- package/dist/config-types.mjs.map +1 -0
- package/dist/contract-builder.d.mts +769 -0
- package/dist/contract-builder.d.mts.map +1 -0
- package/dist/contract-builder.mjs +1288 -0
- package/dist/contract-builder.mjs.map +1 -0
- package/package.json +20 -17
- package/schemas/data-contract-sql-v1.json +189 -23
- package/src/authoring-helper-runtime.ts +139 -0
- package/src/authoring-type-utils.ts +168 -0
- package/src/build-contract.ts +463 -0
- package/src/composed-authoring-helpers.ts +256 -0
- package/src/config-types.ts +11 -0
- package/src/contract-builder.ts +232 -551
- package/src/contract-definition.ts +103 -0
- package/src/contract-dsl.ts +1492 -0
- package/src/contract-lowering.ts +703 -0
- package/src/contract-types.ts +534 -0
- package/src/contract-warnings.ts +242 -0
- package/src/exports/config-types.ts +2 -0
- package/src/exports/contract-builder.ts +23 -2
- package/dist/chunk-HTNUNGA2.js +0 -346
- package/dist/chunk-HTNUNGA2.js.map +0 -1
- package/dist/contract-builder.d.ts +0 -101
- package/dist/contract-builder.d.ts.map +0 -1
- package/dist/contract.d.ts +0 -50
- package/dist/contract.d.ts.map +0 -1
- package/dist/exports/contract-builder.d.ts +0 -3
- package/dist/exports/contract-builder.d.ts.map +0 -1
- package/dist/exports/contract-builder.js +0 -231
- package/dist/exports/contract-builder.js.map +0 -1
- package/dist/exports/contract.d.ts +0 -2
- package/dist/exports/contract.d.ts.map +0 -1
- package/dist/exports/contract.js +0 -9
- package/dist/exports/contract.js.map +0 -1
- package/src/contract.ts +0 -582
- package/src/exports/contract.ts +0 -1
|
@@ -0,0 +1,1288 @@
|
|
|
1
|
+
import { computeExecutionHash, computeProfileHash, computeStorageHash } from "@prisma-next/contract/hashing";
|
|
2
|
+
import { coreHash } from "@prisma-next/contract/types";
|
|
3
|
+
import { applyFkDefaults } from "@prisma-next/sql-contract/types";
|
|
4
|
+
import { validateStorageSemantics } from "@prisma-next/sql-contract/validators";
|
|
5
|
+
import { ifDefined } from "@prisma-next/utils/defined";
|
|
6
|
+
import { instantiateAuthoringFieldPreset, instantiateAuthoringTypeConstructor, isAuthoringFieldPresetDescriptor, isAuthoringTypeConstructorDescriptor, validateAuthoringHelperArguments } from "@prisma-next/framework-components/authoring";
|
|
7
|
+
|
|
8
|
+
//#region src/build-contract.ts
|
|
9
|
+
function encodeDefaultLiteralValue(value, codecId, codecLookup) {
|
|
10
|
+
const codec = codecLookup?.get(codecId);
|
|
11
|
+
if (codec) return codec.encodeJson(value);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function encodeColumnDefault(defaultInput, codecId, codecLookup) {
|
|
15
|
+
if (defaultInput.kind === "function") return {
|
|
16
|
+
kind: "function",
|
|
17
|
+
expression: defaultInput.expression
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
kind: "literal",
|
|
21
|
+
value: encodeDefaultLiteralValue(defaultInput.value, codecId, codecLookup)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function assertStorageSemantics(storage) {
|
|
25
|
+
const semanticErrors = validateStorageSemantics(storage);
|
|
26
|
+
if (semanticErrors.length > 0) throw new Error(`Contract semantic validation failed: ${semanticErrors.join("; ")}`);
|
|
27
|
+
}
|
|
28
|
+
function assertKnownTargetModel(modelsByName, sourceModelName, targetModelName, context) {
|
|
29
|
+
const targetModel = modelsByName.get(targetModelName);
|
|
30
|
+
if (!targetModel) throw new Error(`${context} on model "${sourceModelName}" references unknown model "${targetModelName}"`);
|
|
31
|
+
return targetModel;
|
|
32
|
+
}
|
|
33
|
+
function assertTargetTableMatches(sourceModelName, targetModel, referencedTableName, context) {
|
|
34
|
+
if (targetModel.tableName !== referencedTableName) throw new Error(`${context} on model "${sourceModelName}" references table "${referencedTableName}" but model "${targetModel.modelName}" maps to "${targetModel.tableName}"`);
|
|
35
|
+
}
|
|
36
|
+
function isValueObjectField(field$1) {
|
|
37
|
+
return "valueObjectName" in field$1;
|
|
38
|
+
}
|
|
39
|
+
const JSONB_CODEC_ID = "pg/jsonb@1";
|
|
40
|
+
const JSONB_NATIVE_TYPE = "jsonb";
|
|
41
|
+
function buildStorageColumn(field$1, codecLookup) {
|
|
42
|
+
if (isValueObjectField(field$1)) {
|
|
43
|
+
const encodedDefault$1 = field$1.default !== void 0 ? encodeColumnDefault(field$1.default, JSONB_CODEC_ID, codecLookup) : void 0;
|
|
44
|
+
return {
|
|
45
|
+
nativeType: JSONB_NATIVE_TYPE,
|
|
46
|
+
codecId: JSONB_CODEC_ID,
|
|
47
|
+
nullable: field$1.nullable,
|
|
48
|
+
...ifDefined("default", encodedDefault$1)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (field$1.many) return {
|
|
52
|
+
nativeType: JSONB_NATIVE_TYPE,
|
|
53
|
+
codecId: JSONB_CODEC_ID,
|
|
54
|
+
nullable: field$1.nullable
|
|
55
|
+
};
|
|
56
|
+
const codecId = field$1.descriptor.codecId;
|
|
57
|
+
const encodedDefault = field$1.default !== void 0 ? encodeColumnDefault(field$1.default, codecId, codecLookup) : void 0;
|
|
58
|
+
return {
|
|
59
|
+
nativeType: field$1.descriptor.nativeType,
|
|
60
|
+
codecId,
|
|
61
|
+
nullable: field$1.nullable,
|
|
62
|
+
...ifDefined("typeParams", field$1.descriptor.typeParams),
|
|
63
|
+
...ifDefined("default", encodedDefault),
|
|
64
|
+
...ifDefined("typeRef", field$1.descriptor.typeRef)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function buildDomainField(field$1, column) {
|
|
68
|
+
if (isValueObjectField(field$1)) return {
|
|
69
|
+
type: {
|
|
70
|
+
kind: "valueObject",
|
|
71
|
+
name: field$1.valueObjectName
|
|
72
|
+
},
|
|
73
|
+
nullable: field$1.nullable,
|
|
74
|
+
...field$1.many ? { many: true } : {}
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
type: {
|
|
78
|
+
kind: "scalar",
|
|
79
|
+
codecId: column.codecId,
|
|
80
|
+
...ifDefined("typeParams", column.typeParams)
|
|
81
|
+
},
|
|
82
|
+
nullable: column.nullable,
|
|
83
|
+
...field$1.many ? { many: true } : {}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function buildSqlContractFromDefinition(definition, codecLookup) {
|
|
87
|
+
const target = definition.target.targetId;
|
|
88
|
+
const targetFamily = "sql";
|
|
89
|
+
const modelsByName = new Map(definition.models.map((m) => [m.modelName, m]));
|
|
90
|
+
const storageTables = {};
|
|
91
|
+
const executionDefaults = [];
|
|
92
|
+
const models = {};
|
|
93
|
+
const roots = {};
|
|
94
|
+
for (const semanticModel of definition.models) {
|
|
95
|
+
const tableName = semanticModel.tableName;
|
|
96
|
+
roots[tableName] = semanticModel.modelName;
|
|
97
|
+
const columns = {};
|
|
98
|
+
const fieldToColumn = {};
|
|
99
|
+
const domainFields = {};
|
|
100
|
+
const domainFieldRefs = {};
|
|
101
|
+
for (const field$1 of semanticModel.fields) {
|
|
102
|
+
if (field$1.executionDefault) {
|
|
103
|
+
if (field$1.default !== void 0) throw new Error(`Field "${semanticModel.modelName}.${field$1.fieldName}" cannot define both default and executionDefault.`);
|
|
104
|
+
if (field$1.nullable) throw new Error(`Field "${semanticModel.modelName}.${field$1.fieldName}" cannot be nullable when executionDefault is present.`);
|
|
105
|
+
}
|
|
106
|
+
const column = buildStorageColumn(field$1, codecLookup);
|
|
107
|
+
columns[field$1.columnName] = column;
|
|
108
|
+
fieldToColumn[field$1.fieldName] = field$1.columnName;
|
|
109
|
+
domainFields[field$1.fieldName] = buildDomainField(field$1, column);
|
|
110
|
+
if (isValueObjectField(field$1)) domainFieldRefs[field$1.fieldName] = {
|
|
111
|
+
kind: "valueObject",
|
|
112
|
+
name: field$1.valueObjectName,
|
|
113
|
+
...field$1.many ? { many: true } : {}
|
|
114
|
+
};
|
|
115
|
+
else if (field$1.many) domainFieldRefs[field$1.fieldName] = {
|
|
116
|
+
kind: "scalar",
|
|
117
|
+
many: true
|
|
118
|
+
};
|
|
119
|
+
if ("executionDefault" in field$1 && field$1.executionDefault) executionDefaults.push({
|
|
120
|
+
ref: {
|
|
121
|
+
table: tableName,
|
|
122
|
+
column: field$1.columnName
|
|
123
|
+
},
|
|
124
|
+
onCreate: field$1.executionDefault
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (semanticModel.id) {
|
|
128
|
+
const fieldsByColumnName = new Map(semanticModel.fields.map((field$1) => [field$1.columnName, field$1]));
|
|
129
|
+
for (const columnName of semanticModel.id.columns) {
|
|
130
|
+
const field$1 = fieldsByColumnName.get(columnName);
|
|
131
|
+
if (field$1?.nullable) throw new Error(`Model "${semanticModel.modelName}" uses nullable field "${field$1.fieldName}" in its identity.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const foreignKeys = (semanticModel.foreignKeys ?? []).map((fk) => {
|
|
135
|
+
const targetModel = assertKnownTargetModel(modelsByName, semanticModel.modelName, fk.references.model, "Foreign key");
|
|
136
|
+
assertTargetTableMatches(semanticModel.modelName, targetModel, fk.references.table, "Foreign key");
|
|
137
|
+
return {
|
|
138
|
+
columns: fk.columns,
|
|
139
|
+
references: {
|
|
140
|
+
table: fk.references.table,
|
|
141
|
+
columns: fk.references.columns
|
|
142
|
+
},
|
|
143
|
+
...applyFkDefaults({
|
|
144
|
+
...ifDefined("constraint", fk.constraint),
|
|
145
|
+
...ifDefined("index", fk.index)
|
|
146
|
+
}, definition.foreignKeyDefaults),
|
|
147
|
+
...ifDefined("name", fk.name),
|
|
148
|
+
...ifDefined("onDelete", fk.onDelete),
|
|
149
|
+
...ifDefined("onUpdate", fk.onUpdate)
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
storageTables[tableName] = {
|
|
153
|
+
columns,
|
|
154
|
+
uniques: (semanticModel.uniques ?? []).map((u) => ({
|
|
155
|
+
columns: u.columns,
|
|
156
|
+
...ifDefined("name", u.name)
|
|
157
|
+
})),
|
|
158
|
+
indexes: (semanticModel.indexes ?? []).map((i) => ({
|
|
159
|
+
columns: i.columns,
|
|
160
|
+
...ifDefined("name", i.name),
|
|
161
|
+
...ifDefined("using", i.using),
|
|
162
|
+
...ifDefined("config", i.config)
|
|
163
|
+
})),
|
|
164
|
+
foreignKeys,
|
|
165
|
+
...semanticModel.id ? { primaryKey: {
|
|
166
|
+
columns: semanticModel.id.columns,
|
|
167
|
+
...ifDefined("name", semanticModel.id.name)
|
|
168
|
+
} } : {}
|
|
169
|
+
};
|
|
170
|
+
const storageFields = {};
|
|
171
|
+
for (const [fieldName, columnName] of Object.entries(fieldToColumn)) storageFields[fieldName] = { column: columnName };
|
|
172
|
+
const columnToField = new Map(Object.entries(fieldToColumn).map(([field$1, col]) => [col, field$1]));
|
|
173
|
+
const modelRelations = {};
|
|
174
|
+
for (const relation of semanticModel.relations ?? []) {
|
|
175
|
+
const targetModel = assertKnownTargetModel(modelsByName, semanticModel.modelName, relation.toModel, "Relation");
|
|
176
|
+
assertTargetTableMatches(semanticModel.modelName, targetModel, relation.toTable, "Relation");
|
|
177
|
+
if (relation.cardinality === "N:M" && !relation.through) throw new Error(`Relation "${semanticModel.modelName}.${relation.fieldName}" with cardinality "N:M" requires through metadata`);
|
|
178
|
+
const targetColumnToField = new Map(targetModel.fields.map((f) => [f.columnName, f.fieldName]));
|
|
179
|
+
modelRelations[relation.fieldName] = {
|
|
180
|
+
to: relation.toModel,
|
|
181
|
+
cardinality: relation.cardinality,
|
|
182
|
+
on: {
|
|
183
|
+
localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
|
|
184
|
+
targetFields: relation.on.childColumns.map((col) => targetColumnToField.get(col) ?? col)
|
|
185
|
+
},
|
|
186
|
+
...relation.through ? { through: {
|
|
187
|
+
table: relation.through.table,
|
|
188
|
+
parentCols: relation.through.parentColumns,
|
|
189
|
+
childCols: relation.through.childColumns
|
|
190
|
+
} } : void 0
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
models[semanticModel.modelName] = {
|
|
194
|
+
storage: {
|
|
195
|
+
table: tableName,
|
|
196
|
+
fields: storageFields
|
|
197
|
+
},
|
|
198
|
+
fields: domainFields,
|
|
199
|
+
relations: modelRelations
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const storageWithoutHash = {
|
|
203
|
+
tables: storageTables,
|
|
204
|
+
types: definition.storageTypes ?? {}
|
|
205
|
+
};
|
|
206
|
+
const storageHash = definition.storageHash ? coreHash(definition.storageHash) : computeStorageHash({
|
|
207
|
+
target,
|
|
208
|
+
targetFamily,
|
|
209
|
+
storage: storageWithoutHash
|
|
210
|
+
});
|
|
211
|
+
const storage = {
|
|
212
|
+
...storageWithoutHash,
|
|
213
|
+
storageHash
|
|
214
|
+
};
|
|
215
|
+
const executionSection = executionDefaults.length > 0 ? { mutations: { defaults: executionDefaults.sort((a, b) => {
|
|
216
|
+
const tableCompare = a.ref.table.localeCompare(b.ref.table);
|
|
217
|
+
if (tableCompare !== 0) return tableCompare;
|
|
218
|
+
return a.ref.column.localeCompare(b.ref.column);
|
|
219
|
+
}) } } : void 0;
|
|
220
|
+
const extensionNamespaces = definition.extensionPacks ? Object.values(definition.extensionPacks).map((pack) => pack.id) : void 0;
|
|
221
|
+
const extensionPacks = { ...definition.extensionPacks || {} };
|
|
222
|
+
if (extensionNamespaces) {
|
|
223
|
+
for (const namespace of extensionNamespaces) if (!Object.hasOwn(extensionPacks, namespace)) extensionPacks[namespace] = {};
|
|
224
|
+
}
|
|
225
|
+
const capabilities = definition.capabilities || {};
|
|
226
|
+
const profileHash = computeProfileHash({
|
|
227
|
+
target,
|
|
228
|
+
targetFamily,
|
|
229
|
+
capabilities
|
|
230
|
+
});
|
|
231
|
+
const executionWithHash = executionSection ? {
|
|
232
|
+
...executionSection,
|
|
233
|
+
executionHash: computeExecutionHash({
|
|
234
|
+
target,
|
|
235
|
+
targetFamily,
|
|
236
|
+
execution: executionSection
|
|
237
|
+
})
|
|
238
|
+
} : void 0;
|
|
239
|
+
const valueObjects = definition.valueObjects && definition.valueObjects.length > 0 ? Object.fromEntries(definition.valueObjects.map((vo) => [vo.name, { fields: Object.fromEntries(vo.fields.map((f) => [f.fieldName, isValueObjectField(f) ? {
|
|
240
|
+
type: {
|
|
241
|
+
kind: "valueObject",
|
|
242
|
+
name: f.valueObjectName
|
|
243
|
+
},
|
|
244
|
+
nullable: f.nullable,
|
|
245
|
+
...f.many ? { many: true } : {}
|
|
246
|
+
} : {
|
|
247
|
+
type: {
|
|
248
|
+
kind: "scalar",
|
|
249
|
+
codecId: f.descriptor.codecId,
|
|
250
|
+
...ifDefined("typeParams", f.descriptor.typeParams)
|
|
251
|
+
},
|
|
252
|
+
nullable: f.nullable
|
|
253
|
+
}])) }])) : void 0;
|
|
254
|
+
const contract = {
|
|
255
|
+
target,
|
|
256
|
+
targetFamily,
|
|
257
|
+
models,
|
|
258
|
+
roots,
|
|
259
|
+
storage,
|
|
260
|
+
...executionWithHash ? { execution: executionWithHash } : {},
|
|
261
|
+
...ifDefined("valueObjects", valueObjects),
|
|
262
|
+
extensionPacks,
|
|
263
|
+
capabilities,
|
|
264
|
+
profileHash,
|
|
265
|
+
meta: {}
|
|
266
|
+
};
|
|
267
|
+
assertStorageSemantics(contract.storage);
|
|
268
|
+
return contract;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/authoring-helper-runtime.ts
|
|
273
|
+
function isNamedConstraintOptionsLike(value) {
|
|
274
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
275
|
+
if (Object.keys(value).some((key) => key !== "name")) return false;
|
|
276
|
+
const name = value.name;
|
|
277
|
+
return name === void 0 || typeof name === "string";
|
|
278
|
+
}
|
|
279
|
+
const blockedSegments = new Set([
|
|
280
|
+
"__proto__",
|
|
281
|
+
"constructor",
|
|
282
|
+
"prototype"
|
|
283
|
+
]);
|
|
284
|
+
function assertSafeHelperKey(key, path) {
|
|
285
|
+
if (blockedSegments.has(key)) throw new Error(`Invalid authoring helper "${[...path, key].join(".")}". Helper path segments must not use "${key}".`);
|
|
286
|
+
}
|
|
287
|
+
function createTypeHelpersFromNamespace(namespace, path = []) {
|
|
288
|
+
const helpers = {};
|
|
289
|
+
for (const [key, value] of Object.entries(namespace)) {
|
|
290
|
+
assertSafeHelperKey(key, path);
|
|
291
|
+
const currentPath = [...path, key];
|
|
292
|
+
if (isAuthoringTypeConstructorDescriptor(value)) {
|
|
293
|
+
const helperPath = currentPath.join(".");
|
|
294
|
+
helpers[key] = (...args) => {
|
|
295
|
+
validateAuthoringHelperArguments(helperPath, value.args, args);
|
|
296
|
+
return instantiateAuthoringTypeConstructor(value, args);
|
|
297
|
+
};
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
helpers[key] = createTypeHelpersFromNamespace(value, currentPath);
|
|
301
|
+
}
|
|
302
|
+
return helpers;
|
|
303
|
+
}
|
|
304
|
+
function createFieldPresetHelper(options) {
|
|
305
|
+
return (...rawArgs) => {
|
|
306
|
+
const acceptsNamedConstraintOptions = options.descriptor.output.id === true || options.descriptor.output.unique === true;
|
|
307
|
+
const declaredArguments = options.descriptor.args ?? [];
|
|
308
|
+
if (acceptsNamedConstraintOptions && rawArgs.length > declaredArguments.length + 1) throw new Error(`${options.helperPath} expects at most ${declaredArguments.length + 1} argument(s), received ${rawArgs.length}`);
|
|
309
|
+
let args = rawArgs;
|
|
310
|
+
let namedConstraintOptions;
|
|
311
|
+
if (acceptsNamedConstraintOptions && rawArgs.length === declaredArguments.length + 1) {
|
|
312
|
+
const maybeNamedConstraintOptions = rawArgs.at(-1);
|
|
313
|
+
if (!isNamedConstraintOptionsLike(maybeNamedConstraintOptions)) throw new Error(`${options.helperPath} accepts an optional trailing { name?: string } constraint options object`);
|
|
314
|
+
namedConstraintOptions = maybeNamedConstraintOptions;
|
|
315
|
+
args = rawArgs.slice(0, -1);
|
|
316
|
+
}
|
|
317
|
+
validateAuthoringHelperArguments(options.helperPath, options.descriptor.args, args);
|
|
318
|
+
return options.build({
|
|
319
|
+
args,
|
|
320
|
+
...namedConstraintOptions ? { namedConstraintOptions } : {}
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function createFieldHelpersFromNamespace(namespace, createLeafHelper, path = []) {
|
|
325
|
+
const helpers = {};
|
|
326
|
+
for (const [key, value] of Object.entries(namespace)) {
|
|
327
|
+
assertSafeHelperKey(key, path);
|
|
328
|
+
const currentPath = [...path, key];
|
|
329
|
+
if (isAuthoringFieldPresetDescriptor(value)) {
|
|
330
|
+
helpers[key] = createLeafHelper({
|
|
331
|
+
helperPath: currentPath.join("."),
|
|
332
|
+
descriptor: value
|
|
333
|
+
});
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
helpers[key] = createFieldHelpersFromNamespace(value, createLeafHelper, currentPath);
|
|
337
|
+
}
|
|
338
|
+
return helpers;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/contract-dsl.ts
|
|
343
|
+
function isColumnDefault(value) {
|
|
344
|
+
if (typeof value !== "object" || value === null) return false;
|
|
345
|
+
const kind = value.kind;
|
|
346
|
+
return kind === "literal" || kind === "function";
|
|
347
|
+
}
|
|
348
|
+
function toColumnDefault(value) {
|
|
349
|
+
if (isColumnDefault(value)) return value;
|
|
350
|
+
return {
|
|
351
|
+
kind: "literal",
|
|
352
|
+
value
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
var ScalarFieldBuilder = class ScalarFieldBuilder {
|
|
356
|
+
constructor(state) {
|
|
357
|
+
this.state = state;
|
|
358
|
+
}
|
|
359
|
+
optional() {
|
|
360
|
+
return new ScalarFieldBuilder({
|
|
361
|
+
...this.state,
|
|
362
|
+
nullable: true
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
column(name) {
|
|
366
|
+
return new ScalarFieldBuilder({
|
|
367
|
+
...this.state,
|
|
368
|
+
columnName: name
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
default(value) {
|
|
372
|
+
return new ScalarFieldBuilder({
|
|
373
|
+
...this.state,
|
|
374
|
+
default: toColumnDefault(value)
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
defaultSql(expression) {
|
|
378
|
+
return new ScalarFieldBuilder({
|
|
379
|
+
...this.state,
|
|
380
|
+
default: {
|
|
381
|
+
kind: "function",
|
|
382
|
+
expression
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
id(options) {
|
|
387
|
+
return new ScalarFieldBuilder({
|
|
388
|
+
...this.state,
|
|
389
|
+
id: options?.name ? { name: options.name } : {}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
unique(options) {
|
|
393
|
+
return new ScalarFieldBuilder({
|
|
394
|
+
...this.state,
|
|
395
|
+
unique: options?.name ? { name: options.name } : {}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
sql(spec) {
|
|
399
|
+
const idSpec = "id" in spec ? spec.id : void 0;
|
|
400
|
+
const uniqueSpec = "unique" in spec ? spec.unique : void 0;
|
|
401
|
+
if (idSpec && !this.state.id) throw new Error("field.sql({ id }) requires an existing inline .id(...) declaration.");
|
|
402
|
+
if (uniqueSpec && !this.state.unique) throw new Error("field.sql({ unique }) requires an existing inline .unique(...) declaration.");
|
|
403
|
+
return new ScalarFieldBuilder({
|
|
404
|
+
...this.state,
|
|
405
|
+
...spec.column ? { columnName: spec.column } : {},
|
|
406
|
+
...idSpec ? { id: { name: idSpec.name } } : {},
|
|
407
|
+
...uniqueSpec ? { unique: { name: uniqueSpec.name } } : {}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
build() {
|
|
411
|
+
return this.state;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
function columnField(descriptor) {
|
|
415
|
+
return new ScalarFieldBuilder({
|
|
416
|
+
kind: "scalar",
|
|
417
|
+
descriptor,
|
|
418
|
+
nullable: false
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function generatedField(spec) {
|
|
422
|
+
return new ScalarFieldBuilder({
|
|
423
|
+
kind: "scalar",
|
|
424
|
+
descriptor: {
|
|
425
|
+
...spec.type,
|
|
426
|
+
...spec.typeParams ? { typeParams: spec.typeParams } : {}
|
|
427
|
+
},
|
|
428
|
+
nullable: false,
|
|
429
|
+
executionDefault: spec.generated
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function namedTypeField(typeRef) {
|
|
433
|
+
return new ScalarFieldBuilder({
|
|
434
|
+
kind: "scalar",
|
|
435
|
+
typeRef,
|
|
436
|
+
nullable: false
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function buildFieldPreset(descriptor, args, namedConstraintOptions) {
|
|
440
|
+
const preset = instantiateAuthoringFieldPreset(descriptor, args);
|
|
441
|
+
return new ScalarFieldBuilder({
|
|
442
|
+
kind: "scalar",
|
|
443
|
+
descriptor: preset.descriptor,
|
|
444
|
+
nullable: preset.nullable,
|
|
445
|
+
...ifDefined("default", preset.default),
|
|
446
|
+
...ifDefined("executionDefault", preset.executionDefault),
|
|
447
|
+
...preset.id ? { id: namedConstraintOptions?.name ? { name: namedConstraintOptions.name } : {} } : {},
|
|
448
|
+
...preset.unique ? { unique: namedConstraintOptions?.name ? { name: namedConstraintOptions.name } : {} } : {}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
var RelationBuilder = class RelationBuilder {
|
|
452
|
+
constructor(state) {
|
|
453
|
+
this.state = state;
|
|
454
|
+
}
|
|
455
|
+
sql(spec) {
|
|
456
|
+
if (this.state.kind !== "belongsTo") throw new Error("relation.sql(...) is only supported for belongsTo relations.");
|
|
457
|
+
return new RelationBuilder({
|
|
458
|
+
...this.state,
|
|
459
|
+
sql: spec
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
build() {
|
|
463
|
+
return this.state;
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
function normalizeFieldRefInput(input) {
|
|
467
|
+
return (Array.isArray(input) ? input : [input]).map((ref) => ref.fieldName);
|
|
468
|
+
}
|
|
469
|
+
function normalizeTargetFieldRefInput(input) {
|
|
470
|
+
const refs = Array.isArray(input) ? input : [input];
|
|
471
|
+
const [first] = refs;
|
|
472
|
+
if (!first) throw new Error("Expected at least one target ref");
|
|
473
|
+
if (refs.some((ref) => ref.modelName !== first.modelName)) throw new Error("All target refs in a foreign key must point to the same model");
|
|
474
|
+
return {
|
|
475
|
+
modelName: first.modelName,
|
|
476
|
+
fieldNames: refs.map((ref) => ref.fieldName),
|
|
477
|
+
source: refs.some((ref) => ref.source === "string") ? "string" : "token"
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function createConstraintsDsl() {
|
|
481
|
+
function ref(modelName, fieldName) {
|
|
482
|
+
return {
|
|
483
|
+
kind: "targetFieldRef",
|
|
484
|
+
source: "string",
|
|
485
|
+
modelName,
|
|
486
|
+
fieldName
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function id(fieldOrFields, options) {
|
|
490
|
+
return {
|
|
491
|
+
kind: "id",
|
|
492
|
+
fields: normalizeFieldRefInput(fieldOrFields),
|
|
493
|
+
...options?.name ? { name: options.name } : {}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function unique(fieldOrFields, options) {
|
|
497
|
+
return {
|
|
498
|
+
kind: "unique",
|
|
499
|
+
fields: normalizeFieldRefInput(fieldOrFields),
|
|
500
|
+
...options?.name ? { name: options.name } : {}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function index(fieldOrFields, options) {
|
|
504
|
+
return {
|
|
505
|
+
kind: "index",
|
|
506
|
+
fields: normalizeFieldRefInput(fieldOrFields),
|
|
507
|
+
...options?.name ? { name: options.name } : {},
|
|
508
|
+
...options?.using ? { using: options.using } : {},
|
|
509
|
+
...options?.config ? { config: options.config } : {}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function foreignKey(fieldOrFields, target, options) {
|
|
513
|
+
const normalizedTarget = normalizeTargetFieldRefInput(target);
|
|
514
|
+
return {
|
|
515
|
+
kind: "fk",
|
|
516
|
+
fields: normalizeFieldRefInput(fieldOrFields),
|
|
517
|
+
targetModel: normalizedTarget.modelName,
|
|
518
|
+
targetFields: normalizedTarget.fieldNames,
|
|
519
|
+
targetSource: normalizedTarget.source,
|
|
520
|
+
...options?.name ? { name: options.name } : {},
|
|
521
|
+
...options?.onDelete ? { onDelete: options.onDelete } : {},
|
|
522
|
+
...options?.onUpdate ? { onUpdate: options.onUpdate } : {},
|
|
523
|
+
...options?.constraint !== void 0 ? { constraint: options.constraint } : {},
|
|
524
|
+
...options?.index !== void 0 ? { index: options.index } : {}
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
ref,
|
|
529
|
+
id,
|
|
530
|
+
unique,
|
|
531
|
+
index,
|
|
532
|
+
foreignKey
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function createFieldRefs(fields) {
|
|
536
|
+
const refs = {};
|
|
537
|
+
for (const fieldName of Object.keys(fields)) refs[fieldName] = {
|
|
538
|
+
kind: "columnRef",
|
|
539
|
+
fieldName
|
|
540
|
+
};
|
|
541
|
+
return refs;
|
|
542
|
+
}
|
|
543
|
+
function createModelTokenRefs(modelName, fields) {
|
|
544
|
+
const refs = {};
|
|
545
|
+
for (const fieldName of Object.keys(fields)) refs[fieldName] = {
|
|
546
|
+
kind: "targetFieldRef",
|
|
547
|
+
source: "token",
|
|
548
|
+
modelName,
|
|
549
|
+
fieldName
|
|
550
|
+
};
|
|
551
|
+
return refs;
|
|
552
|
+
}
|
|
553
|
+
function buildStageSpec(stageInput, context) {
|
|
554
|
+
if (typeof stageInput === "function") return stageInput(context);
|
|
555
|
+
return stageInput;
|
|
556
|
+
}
|
|
557
|
+
function createAttributeConstraintsDsl() {
|
|
558
|
+
const constraints = createConstraintsDsl();
|
|
559
|
+
return {
|
|
560
|
+
id: constraints.id,
|
|
561
|
+
unique: constraints.unique
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function createSqlConstraintsDsl() {
|
|
565
|
+
const constraints = createConstraintsDsl();
|
|
566
|
+
return {
|
|
567
|
+
index: constraints.index,
|
|
568
|
+
foreignKey: constraints.foreignKey,
|
|
569
|
+
ref: constraints.ref
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function createColumnRefs(fields) {
|
|
573
|
+
return createFieldRefs(fields);
|
|
574
|
+
}
|
|
575
|
+
function findDuplicateRelationName(existingRelations, nextRelations) {
|
|
576
|
+
return Object.keys(nextRelations).find((relationName) => Object.hasOwn(existingRelations, relationName));
|
|
577
|
+
}
|
|
578
|
+
var ContractModelBuilder = class ContractModelBuilder {
|
|
579
|
+
refs;
|
|
580
|
+
constructor(stageOne, attributesFactory, sqlFactory) {
|
|
581
|
+
this.stageOne = stageOne;
|
|
582
|
+
this.attributesFactory = attributesFactory;
|
|
583
|
+
this.sqlFactory = sqlFactory;
|
|
584
|
+
this.refs = stageOne.modelName ? createModelTokenRefs(stageOne.modelName, stageOne.fields) : void 0;
|
|
585
|
+
}
|
|
586
|
+
ref(fieldName) {
|
|
587
|
+
const modelName = this.stageOne.modelName;
|
|
588
|
+
if (!modelName) throw new Error("Model tokens require model(\"ModelName\", ...) before calling .ref(...)");
|
|
589
|
+
return {
|
|
590
|
+
kind: "targetFieldRef",
|
|
591
|
+
source: "token",
|
|
592
|
+
modelName,
|
|
593
|
+
fieldName
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
relations(relations) {
|
|
597
|
+
const duplicateRelationName = findDuplicateRelationName(this.stageOne.relations, relations);
|
|
598
|
+
if (duplicateRelationName) throw new Error(`Model "${this.stageOne.modelName ?? "<anonymous>"}" already defines relation "${duplicateRelationName}".`);
|
|
599
|
+
return new ContractModelBuilder({
|
|
600
|
+
...this.stageOne,
|
|
601
|
+
relations: {
|
|
602
|
+
...this.stageOne.relations,
|
|
603
|
+
...relations
|
|
604
|
+
}
|
|
605
|
+
}, this.attributesFactory, this.sqlFactory);
|
|
606
|
+
}
|
|
607
|
+
attributes(specOrFactory) {
|
|
608
|
+
return new ContractModelBuilder(this.stageOne, specOrFactory, this.sqlFactory);
|
|
609
|
+
}
|
|
610
|
+
sql(specOrFactory) {
|
|
611
|
+
return new ContractModelBuilder(this.stageOne, this.attributesFactory, specOrFactory);
|
|
612
|
+
}
|
|
613
|
+
buildAttributesSpec() {
|
|
614
|
+
if (!this.attributesFactory) return;
|
|
615
|
+
return buildStageSpec(this.attributesFactory, {
|
|
616
|
+
fields: createFieldRefs(this.stageOne.fields),
|
|
617
|
+
constraints: createAttributeConstraintsDsl()
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
buildSqlSpec() {
|
|
621
|
+
if (!this.sqlFactory) return;
|
|
622
|
+
return buildStageSpec(this.sqlFactory, {
|
|
623
|
+
cols: createColumnRefs(this.stageOne.fields),
|
|
624
|
+
constraints: createSqlConstraintsDsl()
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
function isLazyRelationModelName(value) {
|
|
629
|
+
return typeof value === "object" && value !== null && "kind" in value && value.kind === "lazyRelationModelName" && "resolve" in value && typeof value.resolve === "function";
|
|
630
|
+
}
|
|
631
|
+
function resolveNamedModelTokenName(token) {
|
|
632
|
+
const modelName = token.stageOne.modelName;
|
|
633
|
+
if (!modelName) throw new Error("Relation targets require named model tokens. Use model(\"ModelName\", ...) before passing a token to rel.*(...).");
|
|
634
|
+
return modelName;
|
|
635
|
+
}
|
|
636
|
+
function normalizeRelationModelSource(target) {
|
|
637
|
+
if (typeof target === "string") return {
|
|
638
|
+
kind: "relationModelName",
|
|
639
|
+
source: "string",
|
|
640
|
+
modelName: target
|
|
641
|
+
};
|
|
642
|
+
if (typeof target === "function") return {
|
|
643
|
+
kind: "lazyRelationModelName",
|
|
644
|
+
source: "lazyToken",
|
|
645
|
+
resolve: () => resolveNamedModelTokenName(target())
|
|
646
|
+
};
|
|
647
|
+
return {
|
|
648
|
+
kind: "relationModelName",
|
|
649
|
+
source: "token",
|
|
650
|
+
modelName: resolveNamedModelTokenName(target)
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function model(modelNameOrInput, maybeInput) {
|
|
654
|
+
const input = typeof modelNameOrInput === "string" ? maybeInput : modelNameOrInput;
|
|
655
|
+
if (!input) throw new Error("model(\"ModelName\", ...) requires a model definition.");
|
|
656
|
+
return new ContractModelBuilder({
|
|
657
|
+
...typeof modelNameOrInput === "string" ? { modelName: modelNameOrInput } : {},
|
|
658
|
+
fields: input.fields,
|
|
659
|
+
relations: input.relations ?? {}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
function belongsTo(toModel, options) {
|
|
663
|
+
return new RelationBuilder({
|
|
664
|
+
kind: "belongsTo",
|
|
665
|
+
toModel: normalizeRelationModelSource(toModel),
|
|
666
|
+
from: options.from,
|
|
667
|
+
to: options.to
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
function hasMany(toModel, options) {
|
|
671
|
+
return new RelationBuilder({
|
|
672
|
+
kind: "hasMany",
|
|
673
|
+
toModel: normalizeRelationModelSource(toModel),
|
|
674
|
+
by: options.by
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
function hasOne(toModel, options) {
|
|
678
|
+
return new RelationBuilder({
|
|
679
|
+
kind: "hasOne",
|
|
680
|
+
toModel: normalizeRelationModelSource(toModel),
|
|
681
|
+
by: options.by
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
function manyToMany(toModel, options) {
|
|
685
|
+
return new RelationBuilder({
|
|
686
|
+
kind: "manyToMany",
|
|
687
|
+
toModel: normalizeRelationModelSource(toModel),
|
|
688
|
+
through: normalizeRelationModelSource(options.through),
|
|
689
|
+
from: options.from,
|
|
690
|
+
to: options.to
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
const rel = {
|
|
694
|
+
belongsTo,
|
|
695
|
+
hasMany,
|
|
696
|
+
hasOne,
|
|
697
|
+
manyToMany
|
|
698
|
+
};
|
|
699
|
+
const field = {
|
|
700
|
+
column: columnField,
|
|
701
|
+
generated: generatedField,
|
|
702
|
+
namedType: namedTypeField
|
|
703
|
+
};
|
|
704
|
+
function isContractInput(value) {
|
|
705
|
+
if (typeof value !== "object" || value === null || !("target" in value) || !("family" in value)) return false;
|
|
706
|
+
const target = value.target;
|
|
707
|
+
const family = value.family;
|
|
708
|
+
return typeof target === "object" && target !== null && "kind" in target && target.kind === "target" && typeof family === "object" && family !== null && "kind" in family && family.kind === "family";
|
|
709
|
+
}
|
|
710
|
+
function isRelationFieldArray(value) {
|
|
711
|
+
return Array.isArray(value);
|
|
712
|
+
}
|
|
713
|
+
function normalizeRelationFieldNames(value) {
|
|
714
|
+
if (isRelationFieldArray(value)) return value;
|
|
715
|
+
return [value];
|
|
716
|
+
}
|
|
717
|
+
function resolveRelationModelName(value) {
|
|
718
|
+
if (isLazyRelationModelName(value)) return value.resolve();
|
|
719
|
+
return value.modelName;
|
|
720
|
+
}
|
|
721
|
+
function applyNaming(name, strategy) {
|
|
722
|
+
if (!strategy || strategy === "identity") return name;
|
|
723
|
+
let result = "";
|
|
724
|
+
for (let index = 0; index < name.length; index += 1) {
|
|
725
|
+
const char = name[index];
|
|
726
|
+
if (!char) continue;
|
|
727
|
+
const lower = char.toLowerCase();
|
|
728
|
+
if (char !== lower && index > 0) {
|
|
729
|
+
const prev = name[index - 1];
|
|
730
|
+
const next = name[index + 1];
|
|
731
|
+
const prevIsLower = !!prev && prev === prev.toLowerCase();
|
|
732
|
+
const nextIsLower = !!next && next === next.toLowerCase();
|
|
733
|
+
if (prevIsLower || nextIsLower) result += "_";
|
|
734
|
+
}
|
|
735
|
+
result += lower;
|
|
736
|
+
}
|
|
737
|
+
return result;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region src/composed-authoring-helpers.ts
|
|
742
|
+
function extractTypeNamespace(pack) {
|
|
743
|
+
return pack.authoring?.type ?? {};
|
|
744
|
+
}
|
|
745
|
+
function extractFieldNamespace(pack) {
|
|
746
|
+
return pack.authoring?.field ?? {};
|
|
747
|
+
}
|
|
748
|
+
function mergeHelperNamespaces(target, source, path, leafGuard, label) {
|
|
749
|
+
const assertSafePath = (currentPath) => {
|
|
750
|
+
const blockedSegment = currentPath.find((segment) => segment === "__proto__" || segment === "constructor" || segment === "prototype");
|
|
751
|
+
if (blockedSegment) throw new Error(`Invalid authoring ${label} helper "${currentPath.join(".")}". Helper path segments must not use "${blockedSegment}".`);
|
|
752
|
+
};
|
|
753
|
+
for (const [key, sourceValue] of Object.entries(source)) {
|
|
754
|
+
const currentPath = [...path, key];
|
|
755
|
+
assertSafePath(currentPath);
|
|
756
|
+
const hasExistingValue = Object.hasOwn(target, key);
|
|
757
|
+
const existingValue = hasExistingValue ? target[key] : void 0;
|
|
758
|
+
if (!hasExistingValue) {
|
|
759
|
+
target[key] = sourceValue;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const existingIsLeaf = leafGuard(existingValue);
|
|
763
|
+
const sourceIsLeaf = leafGuard(sourceValue);
|
|
764
|
+
if (existingIsLeaf || sourceIsLeaf) throw new Error(`Duplicate authoring ${label} helper "${currentPath.join(".")}". Helper names must be unique across composed packs.`);
|
|
765
|
+
mergeHelperNamespaces(existingValue, sourceValue, currentPath, leafGuard, label);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function composeTypeNamespace(components) {
|
|
769
|
+
const merged = {};
|
|
770
|
+
for (const component of components) {
|
|
771
|
+
const ns = extractTypeNamespace(component);
|
|
772
|
+
if (Object.keys(ns).length > 0) mergeHelperNamespaces(merged, ns, [], isAuthoringTypeConstructorDescriptor, "type");
|
|
773
|
+
}
|
|
774
|
+
return merged;
|
|
775
|
+
}
|
|
776
|
+
function composeFieldNamespace(components) {
|
|
777
|
+
const merged = {};
|
|
778
|
+
for (const component of components) {
|
|
779
|
+
const ns = extractFieldNamespace(component);
|
|
780
|
+
if (Object.keys(ns).length > 0) mergeHelperNamespaces(merged, ns, [], isAuthoringFieldPresetDescriptor, "field");
|
|
781
|
+
}
|
|
782
|
+
return merged;
|
|
783
|
+
}
|
|
784
|
+
function createComposedFieldHelpers(components) {
|
|
785
|
+
const helperNamespace = createFieldHelpersFromNamespace(composeFieldNamespace(components), ({ helperPath, descriptor }) => createFieldPresetHelper({
|
|
786
|
+
helperPath,
|
|
787
|
+
descriptor,
|
|
788
|
+
build: ({ args, namedConstraintOptions }) => buildFieldPreset(descriptor, args, namedConstraintOptions)
|
|
789
|
+
}));
|
|
790
|
+
const coreFieldHelpers = {
|
|
791
|
+
column: field.column,
|
|
792
|
+
generated: field.generated,
|
|
793
|
+
namedType: field.namedType
|
|
794
|
+
};
|
|
795
|
+
const coreHelperNames = new Set(Object.keys(coreFieldHelpers));
|
|
796
|
+
for (const helperName of Object.keys(helperNamespace)) if (coreHelperNames.has(helperName)) throw new Error(`Duplicate authoring field helper "${helperName}". Core field helpers reserve that name.`);
|
|
797
|
+
return {
|
|
798
|
+
...coreFieldHelpers,
|
|
799
|
+
...helperNamespace
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function createComposedAuthoringHelpers(options) {
|
|
803
|
+
const extensionValues = Object.values(options.extensionPacks ?? {});
|
|
804
|
+
const components = [
|
|
805
|
+
options.family,
|
|
806
|
+
options.target,
|
|
807
|
+
...extensionValues
|
|
808
|
+
];
|
|
809
|
+
return {
|
|
810
|
+
field: createComposedFieldHelpers(components),
|
|
811
|
+
model,
|
|
812
|
+
rel,
|
|
813
|
+
type: createTypeHelpersFromNamespace(composeTypeNamespace(components))
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/contract-warnings.ts
|
|
819
|
+
function hasNamedModelToken(models, modelName) {
|
|
820
|
+
return models[modelName]?.stageOne.modelName === modelName;
|
|
821
|
+
}
|
|
822
|
+
function formatFieldSelection(fieldNames) {
|
|
823
|
+
if (fieldNames.length === 1) return `'${fieldNames[0]}'`;
|
|
824
|
+
return `[${fieldNames.map((fieldName) => `'${fieldName}'`).join(", ")}]`;
|
|
825
|
+
}
|
|
826
|
+
function formatTokenFieldSelection(modelName, fieldNames) {
|
|
827
|
+
if (fieldNames.length === 1) return `${modelName}.refs.${fieldNames[0]}`;
|
|
828
|
+
return `[${fieldNames.map((fieldName) => `${modelName}.refs.${fieldName}`).join(", ")}]`;
|
|
829
|
+
}
|
|
830
|
+
function formatConstraintsRefCall(modelName, fieldNames) {
|
|
831
|
+
if (fieldNames.length === 1) return `constraints.ref('${modelName}', '${fieldNames[0]}')`;
|
|
832
|
+
return `[${fieldNames.map((fieldName) => `constraints.ref('${modelName}', '${fieldName}')`).join(", ")}]`;
|
|
833
|
+
}
|
|
834
|
+
function formatRelationModelDisplay(relationModel) {
|
|
835
|
+
if (relationModel.kind === "lazyRelationModelName") return `() => ${relationModel.resolve()}`;
|
|
836
|
+
return relationModel.source === "string" ? `'${relationModel.modelName}'` : relationModel.modelName;
|
|
837
|
+
}
|
|
838
|
+
function formatRelationCall(relation, targetModelDisplay) {
|
|
839
|
+
if (relation.kind === "belongsTo") return `rel.belongsTo(${targetModelDisplay}, { from: ${formatFieldSelection(normalizeRelationFieldNames(relation.from))}, to: ${formatFieldSelection(normalizeRelationFieldNames(relation.to))} })`;
|
|
840
|
+
if (relation.kind === "hasMany" || relation.kind === "hasOne") {
|
|
841
|
+
const by = formatFieldSelection(normalizeRelationFieldNames(relation.by));
|
|
842
|
+
return `rel.${relation.kind}(${targetModelDisplay}, { by: ${by} })`;
|
|
843
|
+
}
|
|
844
|
+
return `rel.manyToMany(${targetModelDisplay}, { through: ${formatRelationModelDisplay(relation.through)}, from: ${formatFieldSelection(normalizeRelationFieldNames(relation.from))}, to: ${formatFieldSelection(normalizeRelationFieldNames(relation.to))} })`;
|
|
845
|
+
}
|
|
846
|
+
function formatManyToManyCallWithThrough(relation, throughDisplay) {
|
|
847
|
+
return `rel.manyToMany(${formatRelationModelDisplay(relation.toModel)}, { through: ${throughDisplay}, from: ${formatFieldSelection(normalizeRelationFieldNames(relation.from))}, to: ${formatFieldSelection(normalizeRelationFieldNames(relation.to))} })`;
|
|
848
|
+
}
|
|
849
|
+
const WARNING_BATCH_THRESHOLD = 5;
|
|
850
|
+
function flushWarnings(warnings) {
|
|
851
|
+
if (warnings.length === 0) return;
|
|
852
|
+
if (warnings.length <= WARNING_BATCH_THRESHOLD) {
|
|
853
|
+
for (const message of warnings) process.emitWarning(message, { code: "PN_CONTRACT_TYPED_FALLBACK_AVAILABLE" });
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
process.emitWarning(`${warnings.length} contract references use string fallbacks where typed alternatives are available. Use named model tokens and typed storage type refs for autocomplete and type safety.
|
|
857
|
+
` + warnings.map((w) => ` - ${w}`).join("\n"), { code: "PN_CONTRACT_TYPED_FALLBACK_AVAILABLE" });
|
|
858
|
+
}
|
|
859
|
+
function formatFallbackWarning(location, current, suggested) {
|
|
860
|
+
return `Contract ${location} uses ${current}. Use ${suggested} when the named model token is available in the same contract to keep typed relation targets and model refs.`;
|
|
861
|
+
}
|
|
862
|
+
function emitTypedNamedTypeFallbackWarnings(models, storageTypes) {
|
|
863
|
+
const warnings = [];
|
|
864
|
+
const warnedFields = /* @__PURE__ */ new Set();
|
|
865
|
+
for (const [modelName, modelDefinition] of Object.entries(models)) for (const [fieldName, fieldBuilder] of Object.entries(modelDefinition.stageOne.fields)) {
|
|
866
|
+
const fieldState = fieldBuilder.build();
|
|
867
|
+
if (typeof fieldState.typeRef !== "string" || !(fieldState.typeRef in storageTypes)) continue;
|
|
868
|
+
const warningKey = `${modelName}.${fieldName}`;
|
|
869
|
+
if (warnedFields.has(warningKey)) continue;
|
|
870
|
+
warnedFields.add(warningKey);
|
|
871
|
+
warnings.push(`Contract field "${modelName}.${fieldName}" uses field.namedType('${fieldState.typeRef}'). Use field.namedType(types.${fieldState.typeRef}) when the storage type is declared in the same contract to keep autocomplete and typed local refs.`);
|
|
872
|
+
}
|
|
873
|
+
flushWarnings(warnings);
|
|
874
|
+
}
|
|
875
|
+
function emitTypedCrossModelFallbackWarnings(collection) {
|
|
876
|
+
const warnings = [];
|
|
877
|
+
const warnedKeys = /* @__PURE__ */ new Set();
|
|
878
|
+
for (const spec of collection.modelSpecs.values()) {
|
|
879
|
+
for (const [relationName, relationBuilder] of Object.entries(spec.relations)) {
|
|
880
|
+
const relation = relationBuilder.build();
|
|
881
|
+
if (relation.toModel.kind === "relationModelName" && relation.toModel.source === "string" && hasNamedModelToken(collection.models, relation.toModel.modelName)) {
|
|
882
|
+
const warningKey = `${spec.modelName}.${relationName}.toModel`;
|
|
883
|
+
if (!warnedKeys.has(warningKey)) {
|
|
884
|
+
warnedKeys.add(warningKey);
|
|
885
|
+
const current = formatRelationCall(relation, `'${relation.toModel.modelName}'`);
|
|
886
|
+
const suggested = formatRelationCall(relation, relation.toModel.modelName);
|
|
887
|
+
warnings.push(formatFallbackWarning(`relation "${spec.modelName}.${relationName}"`, current, suggested));
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (relation.kind === "manyToMany" && relation.through.kind === "relationModelName" && relation.through.source === "string" && hasNamedModelToken(collection.models, relation.through.modelName)) {
|
|
891
|
+
const warningKey = `${spec.modelName}.${relationName}.through`;
|
|
892
|
+
if (!warnedKeys.has(warningKey)) {
|
|
893
|
+
warnedKeys.add(warningKey);
|
|
894
|
+
const current = formatManyToManyCallWithThrough(relation, `'${relation.through.modelName}'`);
|
|
895
|
+
const suggested = formatManyToManyCallWithThrough(relation, relation.through.modelName);
|
|
896
|
+
warnings.push(formatFallbackWarning(`relation "${spec.modelName}.${relationName}"`, current, suggested));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
for (const [foreignKeyIndex, foreignKey] of (spec.sqlSpec?.foreignKeys ?? []).entries()) {
|
|
901
|
+
if (foreignKey.targetSource !== "string" || !hasNamedModelToken(collection.models, foreignKey.targetModel)) continue;
|
|
902
|
+
const warningKey = `${spec.modelName}.sql.foreignKeys.${foreignKeyIndex}`;
|
|
903
|
+
if (warnedKeys.has(warningKey)) continue;
|
|
904
|
+
warnedKeys.add(warningKey);
|
|
905
|
+
const current = formatConstraintsRefCall(foreignKey.targetModel, foreignKey.targetFields);
|
|
906
|
+
const suggested = formatTokenFieldSelection(foreignKey.targetModel, foreignKey.targetFields);
|
|
907
|
+
warnings.push(formatFallbackWarning(`model "${spec.modelName}"`, `${current} in .sql(...)`, suggested));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
flushWarnings(warnings);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region src/contract-lowering.ts
|
|
915
|
+
function buildStorageTypeReverseLookup(storageTypes) {
|
|
916
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
917
|
+
for (const [key, instance] of Object.entries(storageTypes)) lookup.set(instance, key);
|
|
918
|
+
return lookup;
|
|
919
|
+
}
|
|
920
|
+
function resolveFieldDescriptor(modelName, fieldName, fieldState, storageTypes, storageTypeReverseLookup) {
|
|
921
|
+
if ("descriptor" in fieldState && fieldState.descriptor) return fieldState.descriptor;
|
|
922
|
+
if ("typeRef" in fieldState && fieldState.typeRef) {
|
|
923
|
+
const typeRef = typeof fieldState.typeRef === "string" ? fieldState.typeRef : storageTypeReverseLookup.get(fieldState.typeRef);
|
|
924
|
+
if (!typeRef) throw new Error(`Field "${modelName}.${fieldName}" references a storage type instance that is not present in definition.types`);
|
|
925
|
+
const referencedType = storageTypes[typeRef];
|
|
926
|
+
if (!referencedType) throw new Error(`Field "${modelName}.${fieldName}" references unknown storage type "${typeRef}"`);
|
|
927
|
+
return {
|
|
928
|
+
codecId: referencedType.codecId,
|
|
929
|
+
nativeType: referencedType.nativeType,
|
|
930
|
+
typeRef
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
throw new Error(`Field "${modelName}.${fieldName}" does not resolve to a storage descriptor`);
|
|
934
|
+
}
|
|
935
|
+
function mapFieldNamesToColumnNames(modelName, fieldNames, fieldToColumn) {
|
|
936
|
+
return fieldNames.map((fieldName) => {
|
|
937
|
+
const columnName = fieldToColumn[fieldName];
|
|
938
|
+
if (!columnName) throw new Error(`Unknown field "${modelName}.${fieldName}" in contract definition`);
|
|
939
|
+
return columnName;
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
function assertRelationFieldArity(params) {
|
|
943
|
+
if (params.leftFields.length === params.rightFields.length) return;
|
|
944
|
+
throw new Error(`Relation "${params.modelName}.${params.relationName}" maps ${params.leftFields.length} ${params.leftLabel} field(s) to ${params.rightFields.length} ${params.rightLabel} field(s).`);
|
|
945
|
+
}
|
|
946
|
+
function resolveInlineIdConstraint(spec) {
|
|
947
|
+
const inlineIdFields = [];
|
|
948
|
+
let idName;
|
|
949
|
+
for (const [fieldName, fieldBuilder] of Object.entries(spec.fieldBuilders)) {
|
|
950
|
+
const fieldState = fieldBuilder.build();
|
|
951
|
+
if (!fieldState.id) continue;
|
|
952
|
+
inlineIdFields.push(fieldName);
|
|
953
|
+
if (fieldState.id.name) idName = fieldState.id.name;
|
|
954
|
+
}
|
|
955
|
+
if (inlineIdFields.length === 0) return;
|
|
956
|
+
if (inlineIdFields.length > 1) throw new Error(`Model "${spec.modelName}" marks multiple fields with .id(). Use .attributes(...) for compound identities.`);
|
|
957
|
+
const [inlineIdField] = inlineIdFields;
|
|
958
|
+
if (!inlineIdField) return;
|
|
959
|
+
return {
|
|
960
|
+
kind: "id",
|
|
961
|
+
fields: [inlineIdField],
|
|
962
|
+
...idName ? { name: idName } : {}
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function collectInlineUniqueConstraints(spec) {
|
|
966
|
+
const constraints = [];
|
|
967
|
+
for (const [fieldName, fieldBuilder] of Object.entries(spec.fieldBuilders)) {
|
|
968
|
+
const fieldState = fieldBuilder.build();
|
|
969
|
+
if (!fieldState.unique) continue;
|
|
970
|
+
constraints.push({
|
|
971
|
+
kind: "unique",
|
|
972
|
+
fields: [fieldName],
|
|
973
|
+
...fieldState.unique.name ? { name: fieldState.unique.name } : {}
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
return constraints;
|
|
977
|
+
}
|
|
978
|
+
function resolveModelIdConstraint(spec) {
|
|
979
|
+
const inlineId = resolveInlineIdConstraint(spec);
|
|
980
|
+
const attributeId = spec.attributesSpec?.id;
|
|
981
|
+
if (inlineId && attributeId) throw new Error(`Model "${spec.modelName}" defines identity both inline and in .attributes(...). Pick one identity style.`);
|
|
982
|
+
const resolvedId = attributeId ?? inlineId;
|
|
983
|
+
if (resolvedId && resolvedId.fields.length === 0) throw new Error(`Model "${spec.modelName}" defines an empty identity. Add at least one field.`);
|
|
984
|
+
return resolvedId;
|
|
985
|
+
}
|
|
986
|
+
function resolveModelUniqueConstraints(spec) {
|
|
987
|
+
const attributeUniques = spec.attributesSpec?.uniques ?? [];
|
|
988
|
+
for (const unique of attributeUniques) if (unique.fields.length === 0) throw new Error(`Model "${spec.modelName}" defines an empty unique constraint. Add at least one field.`);
|
|
989
|
+
return [...collectInlineUniqueConstraints(spec), ...attributeUniques];
|
|
990
|
+
}
|
|
991
|
+
function resolveRelationForeignKeys(spec, allSpecs) {
|
|
992
|
+
const foreignKeys = [];
|
|
993
|
+
for (const [relationName, relationBuilder] of Object.entries(spec.relations)) {
|
|
994
|
+
const relation = relationBuilder.build();
|
|
995
|
+
if (relation.kind !== "belongsTo" || !relation.sql?.fk) continue;
|
|
996
|
+
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
997
|
+
if (!allSpecs.has(targetModelName)) throw new Error(`Relation "${spec.modelName}.${relationName}" references unknown model "${targetModelName}"`);
|
|
998
|
+
const fields = normalizeRelationFieldNames(relation.from);
|
|
999
|
+
const targetFields = normalizeRelationFieldNames(relation.to);
|
|
1000
|
+
assertRelationFieldArity({
|
|
1001
|
+
modelName: spec.modelName,
|
|
1002
|
+
relationName,
|
|
1003
|
+
leftLabel: "source",
|
|
1004
|
+
leftFields: fields,
|
|
1005
|
+
rightLabel: "target",
|
|
1006
|
+
rightFields: targetFields
|
|
1007
|
+
});
|
|
1008
|
+
foreignKeys.push({
|
|
1009
|
+
kind: "fk",
|
|
1010
|
+
fields,
|
|
1011
|
+
targetModel: targetModelName,
|
|
1012
|
+
targetFields,
|
|
1013
|
+
...relation.sql.fk.name ? { name: relation.sql.fk.name } : {},
|
|
1014
|
+
...relation.sql.fk.onDelete ? { onDelete: relation.sql.fk.onDelete } : {},
|
|
1015
|
+
...relation.sql.fk.onUpdate ? { onUpdate: relation.sql.fk.onUpdate } : {},
|
|
1016
|
+
...relation.sql.fk.constraint !== void 0 ? { constraint: relation.sql.fk.constraint } : {},
|
|
1017
|
+
...relation.sql.fk.index !== void 0 ? { index: relation.sql.fk.index } : {}
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
return foreignKeys;
|
|
1021
|
+
}
|
|
1022
|
+
function resolveRelationAnchorFields(spec) {
|
|
1023
|
+
const idFields = spec.idConstraint?.fields;
|
|
1024
|
+
if (idFields && idFields.length > 0) return idFields;
|
|
1025
|
+
if ("id" in spec.fieldToColumn) return ["id"];
|
|
1026
|
+
throw new Error(`Model "${spec.modelName}" needs an explicit id or an "id" field to anchor non-owning relations`);
|
|
1027
|
+
}
|
|
1028
|
+
function lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs) {
|
|
1029
|
+
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
1030
|
+
const targetSpec = allSpecs.get(targetModelName);
|
|
1031
|
+
if (!targetSpec) throw new Error(`Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`);
|
|
1032
|
+
const fromFields = normalizeRelationFieldNames(relation.from);
|
|
1033
|
+
const toFields = normalizeRelationFieldNames(relation.to);
|
|
1034
|
+
assertRelationFieldArity({
|
|
1035
|
+
modelName: currentSpec.modelName,
|
|
1036
|
+
relationName,
|
|
1037
|
+
leftLabel: "source",
|
|
1038
|
+
leftFields: fromFields,
|
|
1039
|
+
rightLabel: "target",
|
|
1040
|
+
rightFields: toFields
|
|
1041
|
+
});
|
|
1042
|
+
return {
|
|
1043
|
+
fieldName: relationName,
|
|
1044
|
+
toModel: targetModelName,
|
|
1045
|
+
toTable: targetSpec.tableName,
|
|
1046
|
+
cardinality: "N:1",
|
|
1047
|
+
on: {
|
|
1048
|
+
parentTable: currentSpec.tableName,
|
|
1049
|
+
parentColumns: mapFieldNamesToColumnNames(currentSpec.modelName, fromFields, currentSpec.fieldToColumn),
|
|
1050
|
+
childTable: targetSpec.tableName,
|
|
1051
|
+
childColumns: mapFieldNamesToColumnNames(targetSpec.modelName, toFields, targetSpec.fieldToColumn)
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
function lowerHasOwnershipRelation(relationName, relation, currentSpec, allSpecs) {
|
|
1056
|
+
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
1057
|
+
const targetSpec = allSpecs.get(targetModelName);
|
|
1058
|
+
if (!targetSpec) throw new Error(`Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`);
|
|
1059
|
+
const parentFields = resolveRelationAnchorFields(currentSpec);
|
|
1060
|
+
const childFields = normalizeRelationFieldNames(relation.by);
|
|
1061
|
+
assertRelationFieldArity({
|
|
1062
|
+
modelName: currentSpec.modelName,
|
|
1063
|
+
relationName,
|
|
1064
|
+
leftLabel: "anchor",
|
|
1065
|
+
leftFields: parentFields,
|
|
1066
|
+
rightLabel: "child",
|
|
1067
|
+
rightFields: childFields
|
|
1068
|
+
});
|
|
1069
|
+
return {
|
|
1070
|
+
fieldName: relationName,
|
|
1071
|
+
toModel: targetModelName,
|
|
1072
|
+
toTable: targetSpec.tableName,
|
|
1073
|
+
cardinality: relation.kind === "hasMany" ? "1:N" : "1:1",
|
|
1074
|
+
on: {
|
|
1075
|
+
parentTable: currentSpec.tableName,
|
|
1076
|
+
parentColumns: mapFieldNamesToColumnNames(currentSpec.modelName, parentFields, currentSpec.fieldToColumn),
|
|
1077
|
+
childTable: targetSpec.tableName,
|
|
1078
|
+
childColumns: mapFieldNamesToColumnNames(targetSpec.modelName, childFields, targetSpec.fieldToColumn)
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function lowerManyToManyRelation(relationName, relation, currentSpec, allSpecs) {
|
|
1083
|
+
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
1084
|
+
const targetSpec = allSpecs.get(targetModelName);
|
|
1085
|
+
if (!targetSpec) throw new Error(`Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`);
|
|
1086
|
+
const throughModelName = resolveRelationModelName(relation.through);
|
|
1087
|
+
const throughSpec = allSpecs.get(throughModelName);
|
|
1088
|
+
if (!throughSpec) throw new Error(`Relation "${currentSpec.modelName}.${relationName}" references unknown through model "${throughModelName}"`);
|
|
1089
|
+
const currentAnchorFields = resolveRelationAnchorFields(currentSpec);
|
|
1090
|
+
const targetAnchorFields = resolveRelationAnchorFields(targetSpec);
|
|
1091
|
+
const throughFromFields = normalizeRelationFieldNames(relation.from);
|
|
1092
|
+
const throughToFields = normalizeRelationFieldNames(relation.to);
|
|
1093
|
+
if (currentAnchorFields.length !== throughFromFields.length || targetAnchorFields.length !== throughToFields.length) throw new Error(`Relation "${currentSpec.modelName}.${relationName}" has mismatched many-to-many field counts.`);
|
|
1094
|
+
return {
|
|
1095
|
+
fieldName: relationName,
|
|
1096
|
+
toModel: targetModelName,
|
|
1097
|
+
toTable: targetSpec.tableName,
|
|
1098
|
+
cardinality: "N:M",
|
|
1099
|
+
through: {
|
|
1100
|
+
table: throughSpec.tableName,
|
|
1101
|
+
parentColumns: mapFieldNamesToColumnNames(throughSpec.modelName, throughFromFields, throughSpec.fieldToColumn),
|
|
1102
|
+
childColumns: mapFieldNamesToColumnNames(throughSpec.modelName, throughToFields, throughSpec.fieldToColumn)
|
|
1103
|
+
},
|
|
1104
|
+
on: {
|
|
1105
|
+
parentTable: currentSpec.tableName,
|
|
1106
|
+
parentColumns: mapFieldNamesToColumnNames(currentSpec.modelName, currentAnchorFields, currentSpec.fieldToColumn),
|
|
1107
|
+
childTable: throughSpec.tableName,
|
|
1108
|
+
childColumns: mapFieldNamesToColumnNames(throughSpec.modelName, throughFromFields, throughSpec.fieldToColumn)
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
function resolveRelationNode(relationName, relation, currentSpec, allSpecs) {
|
|
1113
|
+
if (relation.kind === "belongsTo") return lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs);
|
|
1114
|
+
if (relation.kind === "hasMany" || relation.kind === "hasOne") return lowerHasOwnershipRelation(relationName, relation, currentSpec, allSpecs);
|
|
1115
|
+
return lowerManyToManyRelation(relationName, relation, currentSpec, allSpecs);
|
|
1116
|
+
}
|
|
1117
|
+
function lowerForeignKeyNode(spec, targetSpec, foreignKey) {
|
|
1118
|
+
return {
|
|
1119
|
+
columns: mapFieldNamesToColumnNames(spec.modelName, foreignKey.fields, spec.fieldToColumn),
|
|
1120
|
+
references: {
|
|
1121
|
+
model: targetSpec.modelName,
|
|
1122
|
+
table: targetSpec.tableName,
|
|
1123
|
+
columns: mapFieldNamesToColumnNames(targetSpec.modelName, foreignKey.targetFields, targetSpec.fieldToColumn)
|
|
1124
|
+
},
|
|
1125
|
+
...foreignKey.name ? { name: foreignKey.name } : {},
|
|
1126
|
+
...foreignKey.onDelete ? { onDelete: foreignKey.onDelete } : {},
|
|
1127
|
+
...foreignKey.onUpdate ? { onUpdate: foreignKey.onUpdate } : {},
|
|
1128
|
+
...foreignKey.constraint !== void 0 ? { constraint: foreignKey.constraint } : {},
|
|
1129
|
+
...foreignKey.index !== void 0 ? { index: foreignKey.index } : {}
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function resolveForeignKeyNodes(spec, allSpecs) {
|
|
1133
|
+
const relationForeignKeys = resolveRelationForeignKeys(spec, allSpecs).map((foreignKey) => {
|
|
1134
|
+
const targetSpec = allSpecs.get(foreignKey.targetModel);
|
|
1135
|
+
if (!targetSpec) throw new Error(`Foreign key on "${spec.modelName}" references unknown model "${foreignKey.targetModel}"`);
|
|
1136
|
+
return lowerForeignKeyNode(spec, targetSpec, foreignKey);
|
|
1137
|
+
});
|
|
1138
|
+
const sqlForeignKeys = (spec.sqlSpec?.foreignKeys ?? []).map((foreignKey) => {
|
|
1139
|
+
const targetSpec = allSpecs.get(foreignKey.targetModel);
|
|
1140
|
+
if (!targetSpec) throw new Error(`Foreign key on "${spec.modelName}" references unknown model "${foreignKey.targetModel}"`);
|
|
1141
|
+
return lowerForeignKeyNode(spec, targetSpec, foreignKey);
|
|
1142
|
+
});
|
|
1143
|
+
return [...relationForeignKeys, ...sqlForeignKeys];
|
|
1144
|
+
}
|
|
1145
|
+
function resolveModelNode(spec, allSpecs, storageTypes, storageTypeReverseLookup) {
|
|
1146
|
+
const fields = [];
|
|
1147
|
+
for (const [fieldName, fieldBuilder] of Object.entries(spec.fieldBuilders)) {
|
|
1148
|
+
const fieldState = fieldBuilder.build();
|
|
1149
|
+
const descriptor = resolveFieldDescriptor(spec.modelName, fieldName, fieldState, storageTypes, storageTypeReverseLookup);
|
|
1150
|
+
const columnName = spec.fieldToColumn[fieldName];
|
|
1151
|
+
if (!columnName) throw new Error(`Column name resolution failed for "${spec.modelName}.${fieldName}"`);
|
|
1152
|
+
fields.push({
|
|
1153
|
+
fieldName,
|
|
1154
|
+
columnName,
|
|
1155
|
+
descriptor,
|
|
1156
|
+
nullable: fieldState.nullable,
|
|
1157
|
+
...fieldState.default ? { default: fieldState.default } : {},
|
|
1158
|
+
...fieldState.executionDefault ? { executionDefault: fieldState.executionDefault } : {}
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
const { idConstraint } = spec;
|
|
1162
|
+
const uniques = resolveModelUniqueConstraints(spec).map((unique) => ({
|
|
1163
|
+
columns: mapFieldNamesToColumnNames(spec.modelName, unique.fields, spec.fieldToColumn),
|
|
1164
|
+
...unique.name ? { name: unique.name } : {}
|
|
1165
|
+
}));
|
|
1166
|
+
const indexes = (spec.sqlSpec?.indexes ?? []).map((index) => ({
|
|
1167
|
+
columns: mapFieldNamesToColumnNames(spec.modelName, index.fields, spec.fieldToColumn),
|
|
1168
|
+
...index.name ? { name: index.name } : {},
|
|
1169
|
+
...index.using ? { using: index.using } : {},
|
|
1170
|
+
...index.config ? { config: index.config } : {}
|
|
1171
|
+
}));
|
|
1172
|
+
const foreignKeys = resolveForeignKeyNodes(spec, allSpecs);
|
|
1173
|
+
const relations = Object.entries(spec.relations).map(([relationName, relationBuilder]) => resolveRelationNode(relationName, relationBuilder.build(), spec, allSpecs));
|
|
1174
|
+
return {
|
|
1175
|
+
modelName: spec.modelName,
|
|
1176
|
+
tableName: spec.tableName,
|
|
1177
|
+
fields,
|
|
1178
|
+
...idConstraint ? { id: {
|
|
1179
|
+
columns: mapFieldNamesToColumnNames(spec.modelName, idConstraint.fields, spec.fieldToColumn),
|
|
1180
|
+
...idConstraint.name ? { name: idConstraint.name } : {}
|
|
1181
|
+
} } : {},
|
|
1182
|
+
...uniques.length > 0 ? { uniques } : {},
|
|
1183
|
+
...indexes.length > 0 ? { indexes } : {},
|
|
1184
|
+
...foreignKeys.length > 0 ? { foreignKeys } : {},
|
|
1185
|
+
...relations.length > 0 ? { relations } : {}
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
function collectRuntimeModelSpecs(definition) {
|
|
1189
|
+
const storageTypes = { ...definition.types ?? {} };
|
|
1190
|
+
const models = { ...definition.models ?? {} };
|
|
1191
|
+
emitTypedNamedTypeFallbackWarnings(models, storageTypes);
|
|
1192
|
+
const modelSpecs = /* @__PURE__ */ new Map();
|
|
1193
|
+
const tableOwners = /* @__PURE__ */ new Map();
|
|
1194
|
+
for (const [modelName, modelDefinition] of Object.entries(models)) {
|
|
1195
|
+
const tokenModelName = modelDefinition.stageOne.modelName;
|
|
1196
|
+
if (tokenModelName && tokenModelName !== modelName) throw new Error(`Model token "${tokenModelName}" must be assigned to models.${tokenModelName}. Received models.${modelName}.`);
|
|
1197
|
+
const attributesSpec = modelDefinition.buildAttributesSpec();
|
|
1198
|
+
const sqlSpec = modelDefinition.buildSqlSpec();
|
|
1199
|
+
const tableName = sqlSpec?.table ?? applyNaming(modelName, definition.naming?.tables);
|
|
1200
|
+
const existingModel = tableOwners.get(tableName);
|
|
1201
|
+
if (existingModel) throw new Error(`Models "${existingModel}" and "${modelName}" both map to table "${tableName}".`);
|
|
1202
|
+
tableOwners.set(tableName, modelName);
|
|
1203
|
+
const fieldToColumn = {};
|
|
1204
|
+
const columnOwners = /* @__PURE__ */ new Map();
|
|
1205
|
+
for (const [fieldName, fieldBuilder] of Object.entries(modelDefinition.stageOne.fields)) {
|
|
1206
|
+
const columnName = fieldBuilder.build().columnName ?? applyNaming(fieldName, definition.naming?.columns);
|
|
1207
|
+
const existingField = columnOwners.get(columnName);
|
|
1208
|
+
if (existingField) throw new Error(`Model "${modelName}" maps both "${existingField}" and "${fieldName}" to column "${columnName}".`);
|
|
1209
|
+
columnOwners.set(columnName, fieldName);
|
|
1210
|
+
fieldToColumn[fieldName] = columnName;
|
|
1211
|
+
}
|
|
1212
|
+
const fieldBuilders = modelDefinition.stageOne.fields;
|
|
1213
|
+
const idConstraint = resolveModelIdConstraint({
|
|
1214
|
+
modelName,
|
|
1215
|
+
fieldBuilders,
|
|
1216
|
+
attributesSpec
|
|
1217
|
+
});
|
|
1218
|
+
modelSpecs.set(modelName, {
|
|
1219
|
+
modelName,
|
|
1220
|
+
tableName,
|
|
1221
|
+
fieldBuilders,
|
|
1222
|
+
fieldToColumn,
|
|
1223
|
+
relations: modelDefinition.stageOne.relations,
|
|
1224
|
+
attributesSpec,
|
|
1225
|
+
sqlSpec,
|
|
1226
|
+
idConstraint
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
return {
|
|
1230
|
+
storageTypes,
|
|
1231
|
+
models,
|
|
1232
|
+
modelSpecs
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
function lowerModels(collection) {
|
|
1236
|
+
emitTypedCrossModelFallbackWarnings(collection);
|
|
1237
|
+
const storageTypeReverseLookup = buildStorageTypeReverseLookup(collection.storageTypes);
|
|
1238
|
+
return Array.from(collection.modelSpecs.values()).map((spec) => resolveModelNode(spec, collection.modelSpecs, collection.storageTypes, storageTypeReverseLookup));
|
|
1239
|
+
}
|
|
1240
|
+
function buildContractDefinition(definition) {
|
|
1241
|
+
const collection = collectRuntimeModelSpecs(definition);
|
|
1242
|
+
const models = lowerModels(collection);
|
|
1243
|
+
return {
|
|
1244
|
+
target: definition.target,
|
|
1245
|
+
...definition.extensionPacks ? { extensionPacks: definition.extensionPacks } : {},
|
|
1246
|
+
...definition.capabilities ? { capabilities: definition.capabilities } : {},
|
|
1247
|
+
...definition.storageHash ? { storageHash: definition.storageHash } : {},
|
|
1248
|
+
...definition.foreignKeyDefaults ? { foreignKeyDefaults: definition.foreignKeyDefaults } : {},
|
|
1249
|
+
...Object.keys(collection.storageTypes).length > 0 ? { storageTypes: collection.storageTypes } : {},
|
|
1250
|
+
models
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
//#endregion
|
|
1255
|
+
//#region src/contract-builder.ts
|
|
1256
|
+
function validateTargetPackRef(family, target) {
|
|
1257
|
+
if (family.familyId !== "sql") throw new Error(`defineContract only accepts SQL family packs. Received family "${family.familyId}".`);
|
|
1258
|
+
if (target.familyId !== family.familyId) throw new Error(`target pack "${target.id}" targets family "${target.familyId}" but contract family is "${family.familyId}".`);
|
|
1259
|
+
}
|
|
1260
|
+
function validateExtensionPackRefs(target, extensionPacks) {
|
|
1261
|
+
if (!extensionPacks) return;
|
|
1262
|
+
for (const packRef of Object.values(extensionPacks)) {
|
|
1263
|
+
if (packRef.kind !== "extension") throw new Error(`defineContract only accepts extension pack refs in extensionPacks. Received kind "${packRef.kind}".`);
|
|
1264
|
+
if (packRef.familyId !== target.familyId) throw new Error(`extension pack "${packRef.id}" targets family "${packRef.familyId}" but contract target family is "${target.familyId}".`);
|
|
1265
|
+
if (packRef.targetId && packRef.targetId !== target.targetId) throw new Error(`extension pack "${packRef.id}" targets "${packRef.targetId}" but contract target is "${target.targetId}".`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function buildContractFromDsl(definition) {
|
|
1269
|
+
validateTargetPackRef(definition.family, definition.target);
|
|
1270
|
+
validateExtensionPackRefs(definition.target, definition.extensionPacks);
|
|
1271
|
+
return buildSqlContractFromDefinition(buildContractDefinition(definition), definition.codecLookup);
|
|
1272
|
+
}
|
|
1273
|
+
function defineContract(definition, factory) {
|
|
1274
|
+
if (!isContractInput(definition)) throw new TypeError("defineContract expects a contract definition object. Define your contract with defineContract({ family, target, models, ... }).");
|
|
1275
|
+
if (!factory) return buildContractFromDsl(definition);
|
|
1276
|
+
return buildContractFromDsl({
|
|
1277
|
+
...definition,
|
|
1278
|
+
...factory(createComposedAuthoringHelpers({
|
|
1279
|
+
family: definition.family,
|
|
1280
|
+
target: definition.target,
|
|
1281
|
+
extensionPacks: definition.extensionPacks
|
|
1282
|
+
}))
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
//#endregion
|
|
1287
|
+
export { buildSqlContractFromDefinition, defineContract, field, model, rel };
|
|
1288
|
+
//# sourceMappingURL=contract-builder.mjs.map
|