@prisma-next/sql-contract-psl 0.12.0 → 0.13.0-dev.2
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 +10 -2
- package/dist/index.d.mts +11 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{interpreter-QE6eZZof.mjs → interpreter-B_KtZusL.mjs} +439 -52
- package/dist/interpreter-B_KtZusL.mjs.map +1 -0
- package/dist/provider.d.mts +5 -0
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +6 -3
- package/dist/provider.mjs.map +1 -1
- package/package.json +14 -14
- package/src/interpreter.ts +552 -52
- package/src/provider.ts +11 -1
- package/src/psl-attribute-parsing.ts +66 -0
- package/src/psl-column-resolution.ts +10 -3
- package/src/psl-field-resolution.ts +28 -3
- package/src/psl-relation-resolution.ts +6 -4
- package/dist/interpreter-QE6eZZof.mjs.map +0 -1
package/src/provider.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import type { ContractConfig } from '@prisma-next/config/config-types';
|
|
3
|
+
import { applySpecifierDefaultControlPolicy } from '@prisma-next/contract/apply-specifier-default-control-policy';
|
|
4
|
+
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
3
5
|
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
4
6
|
import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';
|
|
7
|
+
import type { Namespace } from '@prisma-next/framework-components/ir';
|
|
5
8
|
import { parsePslDocument } from '@prisma-next/psl-parser';
|
|
9
|
+
import type { SqlNamespaceTablesInput } from '@prisma-next/sql-contract/types';
|
|
6
10
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
7
11
|
import { notOk, ok } from '@prisma-next/utils/result';
|
|
8
12
|
import { basename, extname } from 'pathe';
|
|
@@ -13,6 +17,8 @@ export interface PrismaContractOptions {
|
|
|
13
17
|
readonly output?: string;
|
|
14
18
|
readonly target: TargetPackRef<'sql', string>;
|
|
15
19
|
readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', string>[];
|
|
20
|
+
readonly createNamespace?: (input: SqlNamespaceTablesInput) => Namespace;
|
|
21
|
+
readonly defaultControlPolicy?: ControlPolicy;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/**
|
|
@@ -99,6 +105,7 @@ export function prismaContract(schemaPath: string, options: PrismaContractOption
|
|
|
99
105
|
? [...context.composedExtensionPacks]
|
|
100
106
|
: undefined,
|
|
101
107
|
),
|
|
108
|
+
composedExtensionContracts: context.composedExtensionContracts,
|
|
102
109
|
...ifDefined(
|
|
103
110
|
'composedExtensionPackRefs',
|
|
104
111
|
options.composedExtensionPackRefs?.length
|
|
@@ -106,12 +113,15 @@ export function prismaContract(schemaPath: string, options: PrismaContractOption
|
|
|
106
113
|
: undefined,
|
|
107
114
|
),
|
|
108
115
|
controlMutationDefaults: context.controlMutationDefaults,
|
|
116
|
+
...ifDefined('createNamespace', options.createNamespace),
|
|
109
117
|
});
|
|
110
118
|
if (!interpreted.ok) {
|
|
111
119
|
return interpreted;
|
|
112
120
|
}
|
|
113
121
|
|
|
114
|
-
return ok(
|
|
122
|
+
return ok(
|
|
123
|
+
applySpecifierDefaultControlPolicy(interpreted.value, options.defaultControlPolicy),
|
|
124
|
+
);
|
|
115
125
|
},
|
|
116
126
|
},
|
|
117
127
|
output: options.output ?? defaultOutputFromSchemaPath(schemaPath),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
|
|
2
|
+
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
2
3
|
import type { PslAttribute, PslSpan } from '@prisma-next/psl-parser';
|
|
3
4
|
import { getPositionalArgument, parseQuotedStringLiteral } from '@prisma-next/psl-parser';
|
|
4
5
|
|
|
@@ -411,6 +412,71 @@ export function findDuplicateFieldName(fieldNames: readonly string[]): string |
|
|
|
411
412
|
return undefined;
|
|
412
413
|
}
|
|
413
414
|
|
|
415
|
+
const CONTROL_POLICY_LITERALS = [
|
|
416
|
+
'managed',
|
|
417
|
+
'tolerated',
|
|
418
|
+
'external',
|
|
419
|
+
'observed',
|
|
420
|
+
] as const satisfies readonly ControlPolicy[];
|
|
421
|
+
|
|
422
|
+
const CONTROL_POLICY_LITERAL_SET = new Set<string>(CONTROL_POLICY_LITERALS);
|
|
423
|
+
|
|
424
|
+
function isControlPolicyLiteral(value: string): value is ControlPolicy {
|
|
425
|
+
return CONTROL_POLICY_LITERAL_SET.has(value);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function parseControlPolicyAttribute(input: {
|
|
429
|
+
readonly attribute: PslAttribute;
|
|
430
|
+
readonly sourceId: string;
|
|
431
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
432
|
+
}): ControlPolicy | undefined {
|
|
433
|
+
const namedArgs = input.attribute.args.filter((arg) => arg.kind === 'named');
|
|
434
|
+
if (namedArgs.length > 0) {
|
|
435
|
+
input.diagnostics.push({
|
|
436
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
437
|
+
message:
|
|
438
|
+
'`@@control` does not accept named arguments; pass the policy positionally as `@@control(external)`.',
|
|
439
|
+
sourceId: input.sourceId,
|
|
440
|
+
span: input.attribute.span,
|
|
441
|
+
});
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const positionalArgs = getPositionalArguments(input.attribute);
|
|
446
|
+
if (positionalArgs.length === 0) {
|
|
447
|
+
input.diagnostics.push({
|
|
448
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
449
|
+
message:
|
|
450
|
+
'`@@control` requires exactly one positional argument: `managed`, `tolerated`, `external`, or `observed`.',
|
|
451
|
+
sourceId: input.sourceId,
|
|
452
|
+
span: input.attribute.span,
|
|
453
|
+
});
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
if (positionalArgs.length > 1) {
|
|
457
|
+
input.diagnostics.push({
|
|
458
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
459
|
+
message: `\`@@control\` accepts exactly one positional argument; got ${positionalArgs.length}.`,
|
|
460
|
+
sourceId: input.sourceId,
|
|
461
|
+
span: input.attribute.span,
|
|
462
|
+
});
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const token = unquoteStringLiteral(positionalArgs[0] ?? '').trim();
|
|
467
|
+
if (!isControlPolicyLiteral(token)) {
|
|
468
|
+
input.diagnostics.push({
|
|
469
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
470
|
+
message: `\`@@control\` argument \`${token}\` is not a known policy. Allowed: \`managed\`, \`tolerated\`, \`external\`, \`observed\`.`,
|
|
471
|
+
sourceId: input.sourceId,
|
|
472
|
+
span: input.attribute.span,
|
|
473
|
+
});
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return token;
|
|
478
|
+
}
|
|
479
|
+
|
|
414
480
|
export function mapFieldNamesToColumns(input: {
|
|
415
481
|
readonly modelName: string;
|
|
416
482
|
readonly fieldNames: readonly string[];
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
PslSpan,
|
|
26
26
|
PslTypeConstructorCall,
|
|
27
27
|
} from '@prisma-next/psl-parser';
|
|
28
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
28
29
|
import {
|
|
29
30
|
lowerDefaultFunctionWithRegistry,
|
|
30
31
|
parseDefaultFunctionCall,
|
|
@@ -67,7 +68,9 @@ export function getAuthoringTypeConstructor(
|
|
|
67
68
|
if (typeof current !== 'object' || current === null || Array.isArray(current)) {
|
|
68
69
|
return undefined;
|
|
69
70
|
}
|
|
70
|
-
current =
|
|
71
|
+
current = blindCast<Record<string, unknown>, 'narrowed by preceding typeof/null/array guards'>(
|
|
72
|
+
current,
|
|
73
|
+
)[segment];
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
return isAuthoringTypeConstructorDescriptor(current) ? current : undefined;
|
|
@@ -94,7 +97,9 @@ export function getAuthoringEntity(
|
|
|
94
97
|
if (typeof current !== 'object' || current === null || Array.isArray(current)) {
|
|
95
98
|
return undefined;
|
|
96
99
|
}
|
|
97
|
-
current =
|
|
100
|
+
current = blindCast<Record<string, unknown>, 'narrowed by preceding typeof/null/array guards'>(
|
|
101
|
+
current,
|
|
102
|
+
)[segment];
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
return isAuthoringEntityTypeDescriptor(current) ? current : undefined;
|
|
@@ -115,7 +120,9 @@ export function getAuthoringFieldPreset(
|
|
|
115
120
|
if (typeof current !== 'object' || current === null || Array.isArray(current)) {
|
|
116
121
|
return undefined;
|
|
117
122
|
}
|
|
118
|
-
current =
|
|
123
|
+
current = blindCast<Record<string, unknown>, 'narrowed by preceding typeof/null/array guards'>(
|
|
124
|
+
current,
|
|
125
|
+
)[segment];
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
return isAuthoringFieldPresetDescriptor(current) ? current : undefined;
|
|
@@ -42,6 +42,24 @@ export type ModelNameMapping = {
|
|
|
42
42
|
readonly fieldColumns: Map<string, string>;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* A PSL model paired with its resolved namespace coordinate (undefined when
|
|
47
|
+
* the target leaves the model late-bound). Two models may share a bare name
|
|
48
|
+
* across namespaces, so structures that must distinguish them are keyed by
|
|
49
|
+
* the `(namespaceId, modelName)` coordinate produced by
|
|
50
|
+
* {@link modelCoordinateKey} rather than the bare model name.
|
|
51
|
+
*/
|
|
52
|
+
export type ModelNamespaceEntry = {
|
|
53
|
+
readonly model: PslModel;
|
|
54
|
+
readonly namespaceId: string | undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const MODEL_COORDINATE_SEPARATOR = '\u0000';
|
|
58
|
+
|
|
59
|
+
export function modelCoordinateKey(namespaceId: string, modelName: string): string {
|
|
60
|
+
return `${namespaceId}${MODEL_COORDINATE_SEPARATOR}${modelName}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
export interface CollectResolvedFieldsInput {
|
|
46
64
|
readonly model: PslModel;
|
|
47
65
|
readonly mapping: ModelNameMapping;
|
|
@@ -232,6 +250,12 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
232
250
|
if (isModelField && relationAttribute) {
|
|
233
251
|
continue;
|
|
234
252
|
}
|
|
253
|
+
// Cross-contract-space relation fields (e.g. `supabase:auth.User @relation(...)`) are not
|
|
254
|
+
// local model fields, but they carry a @relation attribute and should be skipped here.
|
|
255
|
+
// Their FK and RelationNode lowering is handled separately in the interpreter.
|
|
256
|
+
if (field.typeContractSpaceId !== undefined && relationAttribute) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
235
259
|
|
|
236
260
|
const isValueObjectField = compositeTypeNames.has(field.typeName);
|
|
237
261
|
const isListField = field.list;
|
|
@@ -418,12 +442,13 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
418
442
|
}
|
|
419
443
|
|
|
420
444
|
export function buildModelMappings(
|
|
421
|
-
|
|
445
|
+
modelEntries: readonly ModelNamespaceEntry[],
|
|
446
|
+
defaultNamespaceId: string,
|
|
422
447
|
diagnostics: ContractSourceDiagnostic[],
|
|
423
448
|
sourceId: string,
|
|
424
449
|
): Map<string, ModelNameMapping> {
|
|
425
450
|
const result = new Map<string, ModelNameMapping>();
|
|
426
|
-
for (const model of
|
|
451
|
+
for (const { model, namespaceId } of modelEntries) {
|
|
427
452
|
const mapAttribute = getAttribute(model.attributes, 'map');
|
|
428
453
|
const tableName = parseMapName({
|
|
429
454
|
attribute: mapAttribute,
|
|
@@ -446,7 +471,7 @@ export function buildModelMappings(
|
|
|
446
471
|
});
|
|
447
472
|
fieldColumns.set(field.name, columnName);
|
|
448
473
|
}
|
|
449
|
-
result.set(model.name, {
|
|
474
|
+
result.set(modelCoordinateKey(namespaceId ?? defaultNamespaceId, model.name), {
|
|
450
475
|
model,
|
|
451
476
|
tableName,
|
|
452
477
|
fieldColumns,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from './psl-attribute-parsing';
|
|
15
15
|
import { checkUncomposedNamespace, reportUncomposedNamespace } from './psl-column-resolution';
|
|
16
16
|
|
|
17
|
-
export const REFERENTIAL_ACTION_MAP = {
|
|
17
|
+
export const REFERENTIAL_ACTION_MAP: Record<string, ReferentialAction | undefined> = {
|
|
18
18
|
NoAction: 'noAction',
|
|
19
19
|
Restrict: 'restrict',
|
|
20
20
|
Cascade: 'cascade',
|
|
@@ -25,7 +25,7 @@ export const REFERENTIAL_ACTION_MAP = {
|
|
|
25
25
|
cascade: 'cascade',
|
|
26
26
|
setNull: 'setNull',
|
|
27
27
|
setDefault: 'setDefault',
|
|
28
|
-
}
|
|
28
|
+
};
|
|
29
29
|
|
|
30
30
|
export type ParsedRelationAttribute = {
|
|
31
31
|
readonly relationName?: string;
|
|
@@ -42,6 +42,8 @@ export type FkRelationMetadata = {
|
|
|
42
42
|
readonly declaringTableName: string;
|
|
43
43
|
readonly targetModelName: string;
|
|
44
44
|
readonly targetTableName: string;
|
|
45
|
+
/** Resolved namespace coordinate of the related model, when known. */
|
|
46
|
+
readonly targetNamespaceId?: string;
|
|
45
47
|
readonly relationName?: string;
|
|
46
48
|
readonly localColumns: readonly string[];
|
|
47
49
|
readonly referencedColumns: readonly string[];
|
|
@@ -71,8 +73,7 @@ export function normalizeReferentialAction(input: {
|
|
|
71
73
|
readonly span: PslSpan;
|
|
72
74
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
73
75
|
}): ReferentialAction | undefined {
|
|
74
|
-
const normalized =
|
|
75
|
-
REFERENTIAL_ACTION_MAP[input.actionToken as keyof typeof REFERENTIAL_ACTION_MAP];
|
|
76
|
+
const normalized = REFERENTIAL_ACTION_MAP[input.actionToken];
|
|
76
77
|
if (normalized) {
|
|
77
78
|
return normalized;
|
|
78
79
|
}
|
|
@@ -252,6 +253,7 @@ export function indexFkRelations(input: {
|
|
|
252
253
|
fieldName: relation.declaringFieldName,
|
|
253
254
|
toModel: relation.targetModelName,
|
|
254
255
|
toTable: relation.targetTableName,
|
|
256
|
+
...ifDefined('toNamespaceId', relation.targetNamespaceId),
|
|
255
257
|
cardinality: 'N:1',
|
|
256
258
|
on: {
|
|
257
259
|
parentTable: relation.declaringTableName,
|