@prisma-next/sql-contract-psl 0.5.0-dev.6 → 0.5.0-dev.60
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-BChe-9vN.mjs} +354 -46
- package/dist/interpreter-BChe-9vN.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 +12 -11
- package/src/interpreter.ts +128 -28
- package/src/provider.ts +2 -2
- package/src/psl-attribute-parsing.ts +14 -5
- package/src/psl-column-resolution.ts +253 -28
- package/src/psl-field-resolution.ts +138 -17
- package/src/psl-relation-resolution.ts +3 -0
- package/dist/interpreter-iFCRN9nb.mjs.map +0 -1
|
@@ -1,11 +1,15 @@
|
|
|
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 {
|
|
4
4
|
AuthoringContributions,
|
|
5
|
+
AuthoringFieldPresetDescriptor,
|
|
5
6
|
AuthoringTypeConstructorDescriptor,
|
|
6
7
|
} from '@prisma-next/framework-components/authoring';
|
|
7
8
|
import {
|
|
9
|
+
hasRegisteredFieldNamespace,
|
|
10
|
+
instantiateAuthoringFieldPreset,
|
|
8
11
|
instantiateAuthoringTypeConstructor,
|
|
12
|
+
isAuthoringFieldPresetDescriptor,
|
|
9
13
|
isAuthoringTypeConstructorDescriptor,
|
|
10
14
|
validateAuthoringHelperArguments,
|
|
11
15
|
} from '@prisma-next/framework-components/authoring';
|
|
@@ -67,6 +71,34 @@ export function getAuthoringTypeConstructor(
|
|
|
67
71
|
return isAuthoringTypeConstructorDescriptor(current) ? current : undefined;
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Walks `authoringContributions.field` segment-by-segment and returns the
|
|
76
|
+
* field-preset descriptor at the resolved path, or `undefined` if no descriptor
|
|
77
|
+
* is registered.
|
|
78
|
+
*
|
|
79
|
+
* Symmetric with `getAuthoringTypeConstructor`. Field presets are strictly
|
|
80
|
+
* richer than type constructors — they can contribute `default` /
|
|
81
|
+
* `executionDefaults` / `id` / `unique` / `nullable` in addition to the
|
|
82
|
+
* `codecId` / `nativeType` / `typeParams` triple. PSL resolution tries field
|
|
83
|
+
* presets first, then falls back to type constructors on miss (see
|
|
84
|
+
* `resolveFieldTypeDescriptor`).
|
|
85
|
+
*/
|
|
86
|
+
export function getAuthoringFieldPreset(
|
|
87
|
+
contributions: AuthoringContributions | undefined,
|
|
88
|
+
path: readonly string[],
|
|
89
|
+
): AuthoringFieldPresetDescriptor | undefined {
|
|
90
|
+
let current: unknown = contributions?.field;
|
|
91
|
+
|
|
92
|
+
for (const segment of path) {
|
|
93
|
+
if (typeof current !== 'object' || current === null || Array.isArray(current)) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
current = (current as Record<string, unknown>)[segment];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return isAuthoringFieldPresetDescriptor(current) ? current : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
70
102
|
/**
|
|
71
103
|
* Returns the namespace prefix of `attributeName` if it references an
|
|
72
104
|
* unrecognized extension namespace, otherwise `undefined`. A namespace is
|
|
@@ -75,16 +107,22 @@ export function getAuthoringTypeConstructor(
|
|
|
75
107
|
* - `db` (native-type spec, always allowed),
|
|
76
108
|
* - the active family id (e.g. `sql`),
|
|
77
109
|
* - the active target id (e.g. `postgres`),
|
|
110
|
+
* - a registered field-preset namespace (e.g. `temporal`),
|
|
78
111
|
* - present in `composedExtensions`.
|
|
79
112
|
*
|
|
80
|
-
* Family/target namespaces are exempted so that e.g. `@sql.foo`
|
|
81
|
-
* PSL_UNSUPPORTED_*_ATTRIBUTE (the attribute isn't defined)
|
|
82
|
-
* PSL_EXTENSION_NAMESPACE_NOT_COMPOSED (the namespace is already
|
|
113
|
+
* Family/target/field-preset namespaces are exempted so that e.g. `@sql.foo`
|
|
114
|
+
* surfaces as PSL_UNSUPPORTED_*_ATTRIBUTE (the attribute isn't defined)
|
|
115
|
+
* rather than PSL_EXTENSION_NAMESPACE_NOT_COMPOSED (the namespace is already
|
|
116
|
+
* composed).
|
|
83
117
|
*/
|
|
84
118
|
export function checkUncomposedNamespace(
|
|
85
119
|
attributeName: string,
|
|
86
120
|
composedExtensions: ReadonlySet<string>,
|
|
87
|
-
context?: {
|
|
121
|
+
context?: {
|
|
122
|
+
readonly familyId?: string;
|
|
123
|
+
readonly targetId?: string;
|
|
124
|
+
readonly authoringContributions?: AuthoringContributions | undefined;
|
|
125
|
+
},
|
|
88
126
|
): string | undefined {
|
|
89
127
|
const dotIndex = attributeName.indexOf('.');
|
|
90
128
|
if (dotIndex <= 0 || dotIndex === attributeName.length - 1) {
|
|
@@ -95,6 +133,7 @@ export function checkUncomposedNamespace(
|
|
|
95
133
|
namespace === 'db' ||
|
|
96
134
|
namespace === context?.familyId ||
|
|
97
135
|
namespace === context?.targetId ||
|
|
136
|
+
hasRegisteredFieldNamespace(context?.authoringContributions, namespace) ||
|
|
98
137
|
composedExtensions.has(namespace)
|
|
99
138
|
) {
|
|
100
139
|
return undefined;
|
|
@@ -126,6 +165,29 @@ export function reportUncomposedNamespace(input: {
|
|
|
126
165
|
});
|
|
127
166
|
}
|
|
128
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Pushes the canonical `PSL_UNKNOWN_FIELD_PRESET` diagnostic when a typoed
|
|
170
|
+
* preset name is referenced inside a registered field-preset namespace. The
|
|
171
|
+
* `data` payload exposes the namespace and full helper path so machine
|
|
172
|
+
* consumers (agents, IDE extensions) don't have to parse the prose.
|
|
173
|
+
*/
|
|
174
|
+
export function reportUnknownFieldPreset(input: {
|
|
175
|
+
readonly entityLabel: string;
|
|
176
|
+
readonly namespace: string;
|
|
177
|
+
readonly helperPath: string;
|
|
178
|
+
readonly sourceId: string;
|
|
179
|
+
readonly span: PslSpan;
|
|
180
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
181
|
+
}): void {
|
|
182
|
+
input.diagnostics.push({
|
|
183
|
+
code: 'PSL_UNKNOWN_FIELD_PRESET',
|
|
184
|
+
message: `${input.entityLabel} references unknown field preset "${input.helperPath}". Check the spelling against the available presets in the "${input.namespace}" namespace.`,
|
|
185
|
+
sourceId: input.sourceId,
|
|
186
|
+
span: input.span,
|
|
187
|
+
data: { namespace: input.namespace, helperPath: input.helperPath },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
129
191
|
export function instantiatePslTypeConstructor(input: {
|
|
130
192
|
readonly call: PslTypeConstructorCall;
|
|
131
193
|
readonly descriptor: AuthoringTypeConstructorDescriptor;
|
|
@@ -200,17 +262,19 @@ export function resolvePslTypeConstructorDescriptor(input: {
|
|
|
200
262
|
return descriptor;
|
|
201
263
|
}
|
|
202
264
|
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
265
|
+
const uncomposedNamespace = checkUncomposedNamespace(
|
|
266
|
+
input.call.path.join('.'),
|
|
267
|
+
input.composedExtensions,
|
|
268
|
+
{
|
|
269
|
+
familyId: input.familyId,
|
|
270
|
+
targetId: input.targetId,
|
|
271
|
+
authoringContributions: input.authoringContributions,
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
if (uncomposedNamespace) {
|
|
211
275
|
reportUncomposedNamespace({
|
|
212
276
|
subjectLabel: `Type constructor "${input.call.path.join('.')}"`,
|
|
213
|
-
namespace,
|
|
277
|
+
namespace: uncomposedNamespace,
|
|
214
278
|
sourceId: input.sourceId,
|
|
215
279
|
span: input.call.span,
|
|
216
280
|
diagnostics: input.diagnostics,
|
|
@@ -227,8 +291,99 @@ export function resolvePslTypeConstructorDescriptor(input: {
|
|
|
227
291
|
});
|
|
228
292
|
}
|
|
229
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Instantiates a field-preset call against its descriptor, coercing PSL AST
|
|
296
|
+
* arguments into the descriptor's typed argument shape and returning the
|
|
297
|
+
* preset's full set of contract contributions.
|
|
298
|
+
*
|
|
299
|
+
* Symmetric with `instantiatePslTypeConstructor` but richer: a field preset
|
|
300
|
+
* can contribute `default`, `executionDefaults`, `id`, `unique`, and
|
|
301
|
+
* `nullable` in addition to the storage-type triple. PSL → typed-args
|
|
302
|
+
* coercion happens here (via `mapPslHelperArgs`) so that
|
|
303
|
+
* `instantiateAuthoringFieldPreset` itself stays typed-input-only and TS
|
|
304
|
+
* keeps its zero-runtime-validation cost.
|
|
305
|
+
*/
|
|
306
|
+
export function instantiatePslFieldPreset(input: {
|
|
307
|
+
readonly call: PslTypeConstructorCall;
|
|
308
|
+
readonly descriptor: AuthoringFieldPresetDescriptor;
|
|
309
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
310
|
+
readonly sourceId: string;
|
|
311
|
+
readonly entityLabel: string;
|
|
312
|
+
}):
|
|
313
|
+
| {
|
|
314
|
+
readonly descriptor: ColumnDescriptor;
|
|
315
|
+
readonly nullable: boolean;
|
|
316
|
+
readonly default?: ColumnDefault;
|
|
317
|
+
readonly executionDefaults?: ExecutionMutationDefaultPhases;
|
|
318
|
+
readonly id: boolean;
|
|
319
|
+
readonly unique: boolean;
|
|
320
|
+
}
|
|
321
|
+
| undefined {
|
|
322
|
+
const helperPath = input.call.path.join('.');
|
|
323
|
+
const args = mapPslHelperArgs({
|
|
324
|
+
args: input.call.args,
|
|
325
|
+
descriptors: input.descriptor.args ?? [],
|
|
326
|
+
helperLabel: `preset "${helperPath}"`,
|
|
327
|
+
span: input.call.span,
|
|
328
|
+
diagnostics: input.diagnostics,
|
|
329
|
+
sourceId: input.sourceId,
|
|
330
|
+
entityLabel: input.entityLabel,
|
|
331
|
+
});
|
|
332
|
+
if (!args) {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
validateAuthoringHelperArguments(helperPath, input.descriptor.args, args);
|
|
338
|
+
const instantiated = instantiateAuthoringFieldPreset(input.descriptor, args);
|
|
339
|
+
return {
|
|
340
|
+
descriptor: {
|
|
341
|
+
codecId: instantiated.descriptor.codecId,
|
|
342
|
+
nativeType: instantiated.descriptor.nativeType,
|
|
343
|
+
...(instantiated.descriptor.typeParams !== undefined
|
|
344
|
+
? { typeParams: instantiated.descriptor.typeParams }
|
|
345
|
+
: {}),
|
|
346
|
+
},
|
|
347
|
+
nullable: instantiated.nullable,
|
|
348
|
+
...(instantiated.default !== undefined ? { default: instantiated.default } : {}),
|
|
349
|
+
...(instantiated.executionDefaults !== undefined
|
|
350
|
+
? { executionDefaults: instantiated.executionDefaults }
|
|
351
|
+
: {}),
|
|
352
|
+
id: instantiated.id,
|
|
353
|
+
unique: instantiated.unique,
|
|
354
|
+
};
|
|
355
|
+
} catch (error) {
|
|
356
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
357
|
+
input.diagnostics.push({
|
|
358
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
359
|
+
message: `${input.entityLabel} preset "${helperPath}" ${message}`,
|
|
360
|
+
sourceId: input.sourceId,
|
|
361
|
+
span: input.call.span,
|
|
362
|
+
});
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Contract contributions a field preset adds beyond the bare storage-type
|
|
369
|
+
* triple. Set when a field is resolved through the field-preset dispatch
|
|
370
|
+
* path; absent when resolved through the type-constructor path or as a
|
|
371
|
+
* scalar/enum/named-type lookup.
|
|
372
|
+
*/
|
|
373
|
+
export type FieldPresetContributions = {
|
|
374
|
+
readonly nullable: boolean;
|
|
375
|
+
readonly id: boolean;
|
|
376
|
+
readonly unique: boolean;
|
|
377
|
+
readonly default?: ColumnDefault;
|
|
378
|
+
readonly executionDefaults?: ExecutionMutationDefaultPhases;
|
|
379
|
+
};
|
|
380
|
+
|
|
230
381
|
export type ResolveFieldTypeResult =
|
|
231
|
-
| {
|
|
382
|
+
| {
|
|
383
|
+
readonly ok: true;
|
|
384
|
+
readonly descriptor: ColumnDescriptor;
|
|
385
|
+
readonly presetContributions?: FieldPresetContributions;
|
|
386
|
+
}
|
|
232
387
|
| { readonly ok: false; readonly alreadyReported: boolean };
|
|
233
388
|
|
|
234
389
|
export function resolveFieldTypeDescriptor(input: {
|
|
@@ -245,18 +400,73 @@ export function resolveFieldTypeDescriptor(input: {
|
|
|
245
400
|
readonly entityLabel: string;
|
|
246
401
|
}): ResolveFieldTypeResult {
|
|
247
402
|
if (input.field.typeConstructor) {
|
|
403
|
+
// Field presets carry richer semantics than type constructors, so a field
|
|
404
|
+
// preset match is the complete answer. Shared composition rejects exact
|
|
405
|
+
// cross-registry collisions before PSL resolution can observe them.
|
|
406
|
+
const presetDescriptor = getAuthoringFieldPreset(
|
|
407
|
+
input.authoringContributions,
|
|
408
|
+
input.field.typeConstructor.path,
|
|
409
|
+
);
|
|
410
|
+
if (presetDescriptor) {
|
|
411
|
+
const instantiated = instantiatePslFieldPreset({
|
|
412
|
+
call: input.field.typeConstructor,
|
|
413
|
+
descriptor: presetDescriptor,
|
|
414
|
+
diagnostics: input.diagnostics,
|
|
415
|
+
sourceId: input.sourceId,
|
|
416
|
+
entityLabel: input.entityLabel,
|
|
417
|
+
});
|
|
418
|
+
if (!instantiated) {
|
|
419
|
+
return { ok: false, alreadyReported: true };
|
|
420
|
+
}
|
|
421
|
+
const presetContributions: FieldPresetContributions = {
|
|
422
|
+
nullable: instantiated.nullable,
|
|
423
|
+
id: instantiated.id,
|
|
424
|
+
unique: instantiated.unique,
|
|
425
|
+
...(instantiated.default !== undefined ? { default: instantiated.default } : {}),
|
|
426
|
+
...(instantiated.executionDefaults !== undefined
|
|
427
|
+
? { executionDefaults: instantiated.executionDefaults }
|
|
428
|
+
: {}),
|
|
429
|
+
};
|
|
430
|
+
return { ok: true, descriptor: instantiated.descriptor, presetContributions };
|
|
431
|
+
}
|
|
432
|
+
|
|
248
433
|
const helperPath = input.field.typeConstructor.path.join('.');
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
434
|
+
const namespacePrefix =
|
|
435
|
+
input.field.typeConstructor.path.length > 1 ? input.field.typeConstructor.path[0] : undefined;
|
|
436
|
+
const typeDescriptor = getAuthoringTypeConstructor(
|
|
437
|
+
input.authoringContributions,
|
|
438
|
+
input.field.typeConstructor.path,
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
if (
|
|
442
|
+
!typeDescriptor &&
|
|
443
|
+
namespacePrefix &&
|
|
444
|
+
hasRegisteredFieldNamespace(input.authoringContributions, namespacePrefix)
|
|
445
|
+
) {
|
|
446
|
+
reportUnknownFieldPreset({
|
|
447
|
+
entityLabel: input.entityLabel,
|
|
448
|
+
namespace: namespacePrefix,
|
|
449
|
+
helperPath,
|
|
450
|
+
sourceId: input.sourceId,
|
|
451
|
+
span: input.field.typeConstructor.span,
|
|
452
|
+
diagnostics: input.diagnostics,
|
|
453
|
+
});
|
|
454
|
+
return { ok: false, alreadyReported: true };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const descriptor =
|
|
458
|
+
typeDescriptor ??
|
|
459
|
+
resolvePslTypeConstructorDescriptor({
|
|
460
|
+
call: input.field.typeConstructor,
|
|
461
|
+
authoringContributions: input.authoringContributions,
|
|
462
|
+
composedExtensions: input.composedExtensions,
|
|
463
|
+
familyId: input.familyId,
|
|
464
|
+
targetId: input.targetId,
|
|
465
|
+
diagnostics: input.diagnostics,
|
|
466
|
+
sourceId: input.sourceId,
|
|
467
|
+
unsupportedCode: 'PSL_UNSUPPORTED_FIELD_TYPE',
|
|
468
|
+
unsupportedMessage: `${input.entityLabel} type constructor "${helperPath}" is not supported in SQL PSL provider v1`,
|
|
469
|
+
});
|
|
260
470
|
if (!descriptor) {
|
|
261
471
|
return { ok: false, alreadyReported: true };
|
|
262
472
|
}
|
|
@@ -495,7 +705,7 @@ export function lowerDefaultForField(input: {
|
|
|
495
705
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
496
706
|
}): {
|
|
497
707
|
readonly defaultValue?: ColumnDefault;
|
|
498
|
-
readonly
|
|
708
|
+
readonly executionDefaults?: ExecutionMutationDefaultPhases;
|
|
499
709
|
} {
|
|
500
710
|
const positionalEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'positional');
|
|
501
711
|
const namedEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'named');
|
|
@@ -568,6 +778,21 @@ export function lowerDefaultForField(input: {
|
|
|
568
778
|
return {};
|
|
569
779
|
}
|
|
570
780
|
|
|
781
|
+
// Preset-only generators (e.g. `timestampNow`) co-register their codec
|
|
782
|
+
// through the preset descriptor, so they don't carry an
|
|
783
|
+
// `applicableCodecIds` list. Such a generator surfacing on the
|
|
784
|
+
// `@default(...)` lowering path is itself the bug — emit a diagnostic
|
|
785
|
+
// pointing the user at the correct authoring surface.
|
|
786
|
+
if (generatorDescriptor.applicableCodecIds === undefined) {
|
|
787
|
+
input.diagnostics.push({
|
|
788
|
+
code: 'PSL_INVALID_DEFAULT_APPLICABILITY',
|
|
789
|
+
message: `Default generator "${generatorDescriptor.id}" is not applicable to "@default(...)" lowering. Use the corresponding field preset (e.g. \`temporal.${generatorDescriptor.id === 'timestampNow' ? 'updatedAt' : generatorDescriptor.id}()\`) instead.`,
|
|
790
|
+
sourceId: input.sourceId,
|
|
791
|
+
span: expressionEntry.span,
|
|
792
|
+
});
|
|
793
|
+
return {};
|
|
794
|
+
}
|
|
795
|
+
|
|
571
796
|
if (!generatorDescriptor.applicableCodecIds.includes(input.columnDescriptor.codecId)) {
|
|
572
797
|
input.diagnostics.push({
|
|
573
798
|
code: 'PSL_INVALID_DEFAULT_APPLICABILITY',
|
|
@@ -578,7 +803,7 @@ export function lowerDefaultForField(input: {
|
|
|
578
803
|
return {};
|
|
579
804
|
}
|
|
580
805
|
|
|
581
|
-
return {
|
|
806
|
+
return { executionDefaults: { onCreate: lowered.value.generated } };
|
|
582
807
|
}
|
|
583
808
|
|
|
584
809
|
export function resolveColumnDescriptor(
|
|
@@ -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;
|
|
@@ -276,15 +366,46 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
276
366
|
sourceId,
|
|
277
367
|
diagnostics,
|
|
278
368
|
});
|
|
369
|
+
let isIdField = Boolean(idAttribute);
|
|
370
|
+
if (idAttribute && field.optional) {
|
|
371
|
+
diagnostics.push({
|
|
372
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
373
|
+
message: `Field "${model.name}.${field.name}" @id cannot be optional; primary key columns must be NOT NULL`,
|
|
374
|
+
sourceId,
|
|
375
|
+
span: idAttribute.span,
|
|
376
|
+
});
|
|
377
|
+
isIdField = false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Field presets contribute their own default / executionDefaults / id /
|
|
381
|
+
// unique. They take precedence over attribute-derived contributions for
|
|
382
|
+
// this field, since a preset *is* the field declaration. Conflicts with
|
|
383
|
+
// `@default` and optional are already rejected above; explicit `@id`
|
|
384
|
+
// would be redundant noise on the resolved field, so we surface it as
|
|
385
|
+
// a hard error here for symmetry.
|
|
386
|
+
if (presetContributions && idAttribute && !presetContributions.id) {
|
|
387
|
+
diagnostics.push({
|
|
388
|
+
code: 'PSL_PRESET_AND_ID_CONFLICT',
|
|
389
|
+
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.`,
|
|
390
|
+
sourceId,
|
|
391
|
+
span: idAttribute.span,
|
|
392
|
+
});
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
279
395
|
|
|
396
|
+
// Field-preset contributions take precedence over attribute-derived
|
|
397
|
+
// sources when present.
|
|
398
|
+
const fieldExecutionDefaults =
|
|
399
|
+
presetContributions?.executionDefaults ?? loweredDefault.executionDefaults;
|
|
400
|
+
const fieldDefaultValue = presetContributions?.default ?? loweredDefault.defaultValue;
|
|
280
401
|
resolvedFields.push({
|
|
281
402
|
field,
|
|
282
403
|
columnName: mappedColumnName,
|
|
283
404
|
descriptor,
|
|
284
|
-
...ifDefined('defaultValue',
|
|
285
|
-
...ifDefined('
|
|
286
|
-
isId: Boolean(
|
|
287
|
-
isUnique: Boolean(uniqueAttribute),
|
|
405
|
+
...ifDefined('defaultValue', fieldDefaultValue),
|
|
406
|
+
...ifDefined('executionDefaults', fieldExecutionDefaults),
|
|
407
|
+
isId: isIdField || Boolean(presetContributions?.id),
|
|
408
|
+
isUnique: Boolean(uniqueAttribute) || Boolean(presetContributions?.unique),
|
|
288
409
|
...ifDefined('idName', idName),
|
|
289
410
|
...ifDefined('uniqueName', uniqueName),
|
|
290
411
|
...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({
|