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