@prisma-next/sql-contract-psl 0.5.0-dev.50 → 0.5.0-dev.52
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 +9 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{interpreter-iFCRN9nb.mjs → interpreter-DJrrH8Ee.mjs} +234 -27
- package/dist/interpreter-DJrrH8Ee.mjs.map +1 -0
- package/dist/provider.d.mts +2 -2
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +1 -1
- package/dist/provider.mjs.map +1 -1
- package/package.json +11 -11
- package/src/interpreter.ts +17 -8
- package/src/provider.ts +2 -2
- package/src/psl-column-resolution.ts +253 -28
- package/src/psl-field-resolution.ts +128 -17
- package/src/psl-relation-resolution.ts +3 -0
- package/dist/interpreter-iFCRN9nb.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
|
|
2
|
-
import type { ColumnDefault,
|
|
2
|
+
import type { ColumnDefault, ExecutionMutationDefaultPhases } from '@prisma-next/contract/types';
|
|
3
3
|
import type { AuthoringContributions } from '@prisma-next/framework-components/authoring';
|
|
4
4
|
import type {
|
|
5
5
|
ControlMutationDefaultRegistry,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
parseConstraintMapArgument,
|
|
14
14
|
parseMapName,
|
|
15
15
|
} from './psl-attribute-parsing';
|
|
16
|
-
import type { ColumnDescriptor } from './psl-column-resolution';
|
|
16
|
+
import type { ColumnDescriptor, FieldPresetContributions } from './psl-column-resolution';
|
|
17
17
|
import {
|
|
18
18
|
checkUncomposedNamespace,
|
|
19
19
|
lowerDefaultForField,
|
|
@@ -26,7 +26,7 @@ export type ResolvedField = {
|
|
|
26
26
|
readonly columnName: string;
|
|
27
27
|
readonly descriptor: ColumnDescriptor;
|
|
28
28
|
readonly defaultValue?: ColumnDefault;
|
|
29
|
-
readonly
|
|
29
|
+
readonly executionDefaults?: ExecutionMutationDefaultPhases;
|
|
30
30
|
readonly isId: boolean;
|
|
31
31
|
readonly isUnique: boolean;
|
|
32
32
|
readonly idName?: string;
|
|
@@ -68,10 +68,54 @@ const BUILTIN_FIELD_ATTRIBUTE_NAMES: ReadonlySet<string> = new Set([
|
|
|
68
68
|
'map',
|
|
69
69
|
]);
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Per-attribute migration rule for attributes that have been removed
|
|
73
|
+
* from PSL in favor of the field-preset surface. The `hint` text is
|
|
74
|
+
* appended to the `PSL_UNSUPPORTED_FIELD_ATTRIBUTE` message so users
|
|
75
|
+
* porting Prisma 6 schemas see "use this preset instead" inline; the
|
|
76
|
+
* `suppressWhen` predicate skips the hint when the user has already
|
|
77
|
+
* migrated (so they don't get told to do what they just did).
|
|
78
|
+
*
|
|
79
|
+
* Pairing the suppression predicate with the hint makes each entry
|
|
80
|
+
* self-contained: a future entry for, say, `@id` ↔ `id.uuidv7()` cannot
|
|
81
|
+
* silently inherit the wrong predicate when added.
|
|
82
|
+
*/
|
|
83
|
+
interface RemovedAttributeRule {
|
|
84
|
+
readonly hint: string;
|
|
85
|
+
readonly suppressWhen: (field: PslField) => boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const REMOVED_ATTRIBUTE_RULES: ReadonlyMap<string, RemovedAttributeRule> = new Map([
|
|
89
|
+
[
|
|
90
|
+
'updatedAt',
|
|
91
|
+
{
|
|
92
|
+
hint: 'Use `temporal.updatedAt()` as a field-preset call instead.',
|
|
93
|
+
suppressWhen: (field) => field.typeConstructor?.path[0] === 'temporal',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
// `validateFieldAttributes` short-circuits on `BUILTIN_FIELD_ATTRIBUTE_NAMES`
|
|
99
|
+
// before consulting `REMOVED_ATTRIBUTE_RULES`. A name appearing in both sets
|
|
100
|
+
// would silently suppress its migration hint, defeating the purpose of the
|
|
101
|
+
// hint table. Fail at module load with a clear message — the table is
|
|
102
|
+
// designed to grow and this is the cheap insurance against future drift.
|
|
103
|
+
{
|
|
104
|
+
const overlap = [...REMOVED_ATTRIBUTE_RULES.keys()].filter((name) =>
|
|
105
|
+
BUILTIN_FIELD_ATTRIBUTE_NAMES.has(name),
|
|
106
|
+
);
|
|
107
|
+
if (overlap.length > 0) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`BUILTIN_FIELD_ATTRIBUTE_NAMES and REMOVED_ATTRIBUTE_RULES must not overlap. Names in both: ${overlap.join(', ')}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
71
114
|
function validateFieldAttributes(input: {
|
|
72
115
|
readonly model: PslModel;
|
|
73
116
|
readonly field: PslField;
|
|
74
117
|
readonly composedExtensions: ReadonlySet<string>;
|
|
118
|
+
readonly authoringContributions: AuthoringContributions | undefined;
|
|
75
119
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
76
120
|
readonly sourceId: string;
|
|
77
121
|
readonly familyId: string;
|
|
@@ -85,6 +129,7 @@ function validateFieldAttributes(input: {
|
|
|
85
129
|
const uncomposedNamespace = checkUncomposedNamespace(attribute.name, input.composedExtensions, {
|
|
86
130
|
familyId: input.familyId,
|
|
87
131
|
targetId: input.targetId,
|
|
132
|
+
authoringContributions: input.authoringContributions,
|
|
88
133
|
});
|
|
89
134
|
if (uncomposedNamespace) {
|
|
90
135
|
reportUncomposedNamespace({
|
|
@@ -97,9 +142,16 @@ function validateFieldAttributes(input: {
|
|
|
97
142
|
continue;
|
|
98
143
|
}
|
|
99
144
|
|
|
145
|
+
const baseMessage = `Field "${input.model.name}.${input.field.name}" uses unsupported attribute "@${attribute.name}"`;
|
|
146
|
+
const removedRule = REMOVED_ATTRIBUTE_RULES.get(attribute.name);
|
|
147
|
+
const message =
|
|
148
|
+
removedRule && !removedRule.suppressWhen(input.field)
|
|
149
|
+
? `${baseMessage}. ${removedRule.hint}`
|
|
150
|
+
: baseMessage;
|
|
151
|
+
|
|
100
152
|
input.diagnostics.push({
|
|
101
153
|
code: 'PSL_UNSUPPORTED_FIELD_ATTRIBUTE',
|
|
102
|
-
message
|
|
154
|
+
message,
|
|
103
155
|
sourceId: input.sourceId,
|
|
104
156
|
span: attribute.span,
|
|
105
157
|
});
|
|
@@ -159,7 +211,9 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
159
211
|
const resolvedFields: ResolvedField[] = [];
|
|
160
212
|
|
|
161
213
|
for (const field of model.fields) {
|
|
162
|
-
|
|
214
|
+
const isModelField = modelNames.has(field.typeName);
|
|
215
|
+
|
|
216
|
+
if (field.list && isModelField) {
|
|
163
217
|
continue;
|
|
164
218
|
}
|
|
165
219
|
|
|
@@ -167,6 +221,7 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
167
221
|
model,
|
|
168
222
|
field,
|
|
169
223
|
composedExtensions,
|
|
224
|
+
authoringContributions,
|
|
170
225
|
diagnostics,
|
|
171
226
|
sourceId,
|
|
172
227
|
familyId,
|
|
@@ -174,7 +229,7 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
174
229
|
});
|
|
175
230
|
|
|
176
231
|
const relationAttribute = getAttribute(field.attributes, 'relation');
|
|
177
|
-
if (
|
|
232
|
+
if (isModelField && relationAttribute) {
|
|
178
233
|
continue;
|
|
179
234
|
}
|
|
180
235
|
|
|
@@ -183,6 +238,7 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
183
238
|
|
|
184
239
|
let descriptor: ColumnDescriptor | undefined;
|
|
185
240
|
let scalarCodecId: string | undefined;
|
|
241
|
+
let presetContributions: FieldPresetContributions | undefined;
|
|
186
242
|
const resolveInput = {
|
|
187
243
|
field,
|
|
188
244
|
enumTypeDescriptors,
|
|
@@ -212,6 +268,17 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
212
268
|
}
|
|
213
269
|
continue;
|
|
214
270
|
}
|
|
271
|
+
// Field presets are complete declarations — they carry their own codec
|
|
272
|
+
// and do not compose with `[]` list-of semantics. Reject early.
|
|
273
|
+
if (resolved.presetContributions) {
|
|
274
|
+
diagnostics.push({
|
|
275
|
+
code: 'PSL_PRESET_NOT_LIST',
|
|
276
|
+
message: `Field "${model.name}.${field.name}" uses a field-preset call as a list element type. Presets cannot be list elements; remove "[]" or use a scalar type.`,
|
|
277
|
+
sourceId,
|
|
278
|
+
span: field.span,
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
215
282
|
scalarCodecId = resolved.descriptor.codecId;
|
|
216
283
|
descriptor = scalarTypeDescriptors.get('Json');
|
|
217
284
|
} else {
|
|
@@ -228,13 +295,37 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
228
295
|
continue;
|
|
229
296
|
}
|
|
230
297
|
descriptor = resolved.descriptor;
|
|
298
|
+
presetContributions = resolved.presetContributions;
|
|
231
299
|
}
|
|
232
300
|
|
|
233
301
|
if (!descriptor) {
|
|
234
302
|
continue;
|
|
235
303
|
}
|
|
236
304
|
|
|
305
|
+
// Field presets are complete declarations: the preset names its own codec
|
|
306
|
+
// and contributes any combination of default / executionDefaults / id /
|
|
307
|
+
// unique. Optional and `@default(...)` modifiers contradict that, so they
|
|
308
|
+
// are hard errors per spec FR7.
|
|
309
|
+
if (presetContributions && field.optional) {
|
|
310
|
+
diagnostics.push({
|
|
311
|
+
code: 'PSL_PRESET_NOT_OPTIONAL',
|
|
312
|
+
message: `Field "${model.name}.${field.name}" uses a field-preset call and cannot be optional. Remove "?" or use a different field type.`,
|
|
313
|
+
sourceId,
|
|
314
|
+
span: field.span,
|
|
315
|
+
});
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
237
319
|
const defaultAttribute = getAttribute(field.attributes, 'default');
|
|
320
|
+
if (presetContributions && defaultAttribute) {
|
|
321
|
+
diagnostics.push({
|
|
322
|
+
code: 'PSL_PRESET_AND_DEFAULT_CONFLICT',
|
|
323
|
+
message: `Field "${model.name}.${field.name}" uses a field-preset call and cannot also declare @default(...). The preset already specifies the default value.`,
|
|
324
|
+
sourceId,
|
|
325
|
+
span: defaultAttribute.span,
|
|
326
|
+
});
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
238
329
|
const loweredDefault = defaultAttribute
|
|
239
330
|
? lowerDefaultForField({
|
|
240
331
|
modelName: model.name,
|
|
@@ -247,11 +338,10 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
247
338
|
diagnostics,
|
|
248
339
|
})
|
|
249
340
|
: {};
|
|
250
|
-
|
|
341
|
+
const loweredOnCreate = loweredDefault.executionDefaults?.onCreate;
|
|
342
|
+
if (field.optional && loweredOnCreate) {
|
|
251
343
|
const generatorDescription =
|
|
252
|
-
|
|
253
|
-
? `"${loweredDefault.executionDefault.id}"`
|
|
254
|
-
: 'for this field';
|
|
344
|
+
loweredOnCreate.kind === 'generator' ? `"${loweredOnCreate.id}"` : 'for this field';
|
|
255
345
|
diagnostics.push({
|
|
256
346
|
code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
|
|
257
347
|
message: `Field "${model.name}.${field.name}" cannot be optional when using execution default ${generatorDescription}. Remove "?" or use a storage default.`,
|
|
@@ -260,10 +350,10 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
260
350
|
});
|
|
261
351
|
continue;
|
|
262
352
|
}
|
|
263
|
-
if (
|
|
264
|
-
const generatorDescriptor = generatorDescriptorById.get(
|
|
353
|
+
if (loweredOnCreate) {
|
|
354
|
+
const generatorDescriptor = generatorDescriptorById.get(loweredOnCreate.id);
|
|
265
355
|
const generatedDescriptor = generatorDescriptor?.resolveGeneratedColumnDescriptor?.({
|
|
266
|
-
generated:
|
|
356
|
+
generated: loweredOnCreate,
|
|
267
357
|
});
|
|
268
358
|
if (generatedDescriptor) {
|
|
269
359
|
descriptor = generatedDescriptor;
|
|
@@ -277,14 +367,35 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
277
367
|
diagnostics,
|
|
278
368
|
});
|
|
279
369
|
|
|
370
|
+
// Field presets contribute their own default / executionDefaults / id /
|
|
371
|
+
// unique. They take precedence over attribute-derived contributions for
|
|
372
|
+
// this field, since a preset *is* the field declaration. Conflicts with
|
|
373
|
+
// `@default` and optional are already rejected above; explicit `@id`
|
|
374
|
+
// would be redundant noise on the resolved field, so we surface it as
|
|
375
|
+
// a hard error here for symmetry.
|
|
376
|
+
if (presetContributions && idAttribute && !presetContributions.id) {
|
|
377
|
+
diagnostics.push({
|
|
378
|
+
code: 'PSL_PRESET_AND_ID_CONFLICT',
|
|
379
|
+
message: `Field "${model.name}.${field.name}" uses a field-preset call and cannot also declare @id. Use a preset that contributes id semantics, or drop @id.`,
|
|
380
|
+
sourceId,
|
|
381
|
+
span: idAttribute.span,
|
|
382
|
+
});
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Field-preset contributions take precedence over attribute-derived
|
|
387
|
+
// sources when present.
|
|
388
|
+
const fieldExecutionDefaults =
|
|
389
|
+
presetContributions?.executionDefaults ?? loweredDefault.executionDefaults;
|
|
390
|
+
const fieldDefaultValue = presetContributions?.default ?? loweredDefault.defaultValue;
|
|
280
391
|
resolvedFields.push({
|
|
281
392
|
field,
|
|
282
393
|
columnName: mappedColumnName,
|
|
283
394
|
descriptor,
|
|
284
|
-
...ifDefined('defaultValue',
|
|
285
|
-
...ifDefined('
|
|
286
|
-
isId: Boolean(idAttribute),
|
|
287
|
-
isUnique: Boolean(uniqueAttribute),
|
|
395
|
+
...ifDefined('defaultValue', fieldDefaultValue),
|
|
396
|
+
...ifDefined('executionDefaults', fieldExecutionDefaults),
|
|
397
|
+
isId: Boolean(idAttribute) || Boolean(presetContributions?.id),
|
|
398
|
+
isUnique: Boolean(uniqueAttribute) || Boolean(presetContributions?.unique),
|
|
288
399
|
...ifDefined('idName', idName),
|
|
289
400
|
...ifDefined('uniqueName', uniqueName),
|
|
290
401
|
...ifDefined('many', isListField ? (true as const) : undefined),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
|
|
2
|
+
import type { AuthoringContributions } from '@prisma-next/framework-components/authoring';
|
|
2
3
|
import type { PslAttribute, PslField, PslSpan } from '@prisma-next/psl-parser';
|
|
3
4
|
import type { ReferentialAction } from '@prisma-next/sql-contract/types';
|
|
4
5
|
import type { RelationNode } from '@prisma-next/sql-contract-ts/contract-builder';
|
|
@@ -334,6 +335,7 @@ export function validateNavigationListFieldAttributes(input: {
|
|
|
334
335
|
readonly field: PslField;
|
|
335
336
|
readonly sourceId: string;
|
|
336
337
|
readonly composedExtensions: Set<string>;
|
|
338
|
+
readonly authoringContributions: AuthoringContributions | undefined;
|
|
337
339
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
338
340
|
readonly familyId: string;
|
|
339
341
|
readonly targetId: string;
|
|
@@ -347,6 +349,7 @@ export function validateNavigationListFieldAttributes(input: {
|
|
|
347
349
|
const uncomposedNamespace = checkUncomposedNamespace(attribute.name, input.composedExtensions, {
|
|
348
350
|
familyId: input.familyId,
|
|
349
351
|
targetId: input.targetId,
|
|
352
|
+
authoringContributions: input.authoringContributions,
|
|
350
353
|
});
|
|
351
354
|
if (uncomposedNamespace) {
|
|
352
355
|
reportUncomposedNamespace({
|