@prisma-next/sql-contract-psl 0.12.0 → 0.13.0-dev.10

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/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(interpreted.value);
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 = (current as Record<string, unknown>)[segment];
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 = (current as Record<string, unknown>)[segment];
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 = (current as Record<string, unknown>)[segment];
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
- models: readonly PslModel[],
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 models) {
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
- } as const;
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,