@prisma-next/sql-contract-psl 0.12.0-dev.5 → 0.12.0-dev.50
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 +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{interpreter-QE6eZZof.mjs → interpreter-Rn_Vzvt-.mjs} +250 -21
- package/dist/interpreter-Rn_Vzvt-.mjs.map +1 -0
- package/dist/provider.d.mts +5 -0
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +5 -3
- package/dist/provider.mjs.map +1 -1
- package/package.json +12 -12
- package/src/interpreter.ts +269 -18
- package/src/provider.ts +10 -1
- package/src/psl-attribute-parsing.ts +66 -0
- package/src/psl-column-resolution.ts +10 -3
- package/src/psl-relation-resolution.ts +3 -4
- package/dist/interpreter-QE6eZZof.mjs.map +0 -1
package/dist/provider.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { t as interpretPslDocumentToSqlContract } from "./interpreter-
|
|
1
|
+
import { t as interpretPslDocumentToSqlContract } from "./interpreter-Rn_Vzvt-.mjs";
|
|
2
2
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
3
3
|
import { notOk, ok } from "@prisma-next/utils/result";
|
|
4
4
|
import { parsePslDocument } from "@prisma-next/psl-parser";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { applySpecifierDefaultControlPolicy } from "@prisma-next/contract/apply-specifier-default-control-policy";
|
|
6
7
|
import { basename, extname } from "pathe";
|
|
7
8
|
//#region src/provider.ts
|
|
8
9
|
/**
|
|
@@ -69,10 +70,11 @@ function prismaContract(schemaPath, options) {
|
|
|
69
70
|
scalarTypeDescriptors,
|
|
70
71
|
...ifDefined("composedExtensionPacks", context.composedExtensionPacks.length > 0 ? [...context.composedExtensionPacks] : void 0),
|
|
71
72
|
...ifDefined("composedExtensionPackRefs", options.composedExtensionPackRefs?.length ? options.composedExtensionPackRefs : void 0),
|
|
72
|
-
controlMutationDefaults: context.controlMutationDefaults
|
|
73
|
+
controlMutationDefaults: context.controlMutationDefaults,
|
|
74
|
+
...ifDefined("createNamespace", options.createNamespace)
|
|
73
75
|
});
|
|
74
76
|
if (!interpreted.ok) return interpreted;
|
|
75
|
-
return ok(interpreted.value);
|
|
77
|
+
return ok(applySpecifierDefaultControlPolicy(interpreted.value, options.defaultControlPolicy));
|
|
76
78
|
}
|
|
77
79
|
},
|
|
78
80
|
output: options.output ?? defaultOutputFromSchemaPath(schemaPath)
|
package/dist/provider.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.mjs","names":[],"sources":["../src/provider.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport type { ContractConfig } from '@prisma-next/config/config-types';\nimport type { CodecLookup } from '@prisma-next/framework-components/codec';\nimport type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';\nimport { parsePslDocument } from '@prisma-next/psl-parser';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { notOk, ok } from '@prisma-next/utils/result';\nimport { basename, extname } from 'pathe';\nimport { interpretPslDocumentToSqlContract } from './interpreter';\nimport type { ColumnDescriptor } from './psl-column-resolution';\n\nexport interface PrismaContractOptions {\n readonly output?: string;\n readonly target: TargetPackRef<'sql', string>;\n readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', string>[];\n}\n\n/**\n * Derives the emit output path from the schema input path so artefacts land\n * colocated with the source (e.g. `src/contract/schema.prisma` →\n * `src/contract/contract.json`). The provider owns this because it is the\n * only layer that knows the input path; the upstream `normalizeContractConfig`\n * default is a last-resort fallback for providers that don't carry one.\n */\nfunction defaultOutputFromSchemaPath(schemaPath: string): string {\n const ext = extname(schemaPath);\n if (ext.length === 0) return `${schemaPath}.json`;\n const base = schemaPath.slice(0, -ext.length);\n // PSL schemas commonly use `schema.prisma`; the emitted JSON is called\n // `contract.json` to mirror the rest of the toolchain, not `schema.json`.\n // Match only the exact basename `schema` so files like `my-schema.prisma`\n // are not silently rewritten to `my-contract.json`.\n if (basename(base) === 'schema') {\n return `${base.slice(0, -'schema'.length)}contract.json`;\n }\n return `${base}.json`;\n}\n\nfunction buildColumnDescriptorMap(\n scalarTypeDescriptors: ReadonlyMap<string, string>,\n codecLookup: CodecLookup,\n): ReadonlyMap<string, ColumnDescriptor> {\n const result = new Map<string, ColumnDescriptor>();\n for (const [typeName, codecId] of scalarTypeDescriptors) {\n const nativeType = codecLookup.targetTypesFor(codecId)?.[0];\n if (nativeType === undefined) continue;\n result.set(typeName, { codecId, nativeType });\n }\n return result;\n}\n\nexport function prismaContract(schemaPath: string, options: PrismaContractOptions): ContractConfig {\n return {\n source: {\n inputs: [schemaPath],\n load: async (context) => {\n const [absoluteSchemaPath] = context.resolvedInputs;\n if (absoluteSchemaPath === undefined) {\n throw new Error(\n 'prismaContract: context.resolvedInputs is empty. The CLI config loader should populate it positional-matched with source.inputs.',\n );\n }\n let schema: string;\n try {\n schema = await readFile(absoluteSchemaPath, 'utf-8');\n } catch (error) {\n const message = String(error);\n return notOk({\n summary: `Failed to read Prisma schema at \"${schemaPath}\"`,\n diagnostics: [\n {\n code: 'PSL_SCHEMA_READ_FAILED',\n message,\n sourceId: schemaPath,\n },\n ],\n meta: { schemaPath, absoluteSchemaPath, cause: message },\n });\n }\n\n const document = parsePslDocument({\n schema,\n sourceId: schemaPath,\n });\n\n const scalarTypeDescriptors = buildColumnDescriptorMap(\n context.scalarTypeDescriptors,\n context.codecLookup,\n );\n\n const interpreted = interpretPslDocumentToSqlContract({\n document,\n target: options.target,\n authoringContributions: context.authoringContributions,\n scalarTypeDescriptors,\n ...ifDefined(\n 'composedExtensionPacks',\n context.composedExtensionPacks.length > 0\n ? [...context.composedExtensionPacks]\n : undefined,\n ),\n ...ifDefined(\n 'composedExtensionPackRefs',\n options.composedExtensionPackRefs?.length\n ? options.composedExtensionPackRefs\n : undefined,\n ),\n controlMutationDefaults: context.controlMutationDefaults,\n });\n if (!interpreted.ok) {\n return interpreted;\n }\n\n return ok(interpreted.value);\n },\n },\n output: options.output ?? defaultOutputFromSchemaPath(schemaPath),\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"provider.mjs","names":[],"sources":["../src/provider.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport type { ContractConfig } from '@prisma-next/config/config-types';\nimport { applySpecifierDefaultControlPolicy } from '@prisma-next/contract/apply-specifier-default-control-policy';\nimport type { ControlPolicy } from '@prisma-next/contract/types';\nimport type { CodecLookup } from '@prisma-next/framework-components/codec';\nimport type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';\nimport type { Namespace } from '@prisma-next/framework-components/ir';\nimport { parsePslDocument } from '@prisma-next/psl-parser';\nimport type { SqlNamespaceTablesInput } from '@prisma-next/sql-contract/types';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { notOk, ok } from '@prisma-next/utils/result';\nimport { basename, extname } from 'pathe';\nimport { interpretPslDocumentToSqlContract } from './interpreter';\nimport type { ColumnDescriptor } from './psl-column-resolution';\n\nexport interface PrismaContractOptions {\n readonly output?: string;\n readonly target: TargetPackRef<'sql', string>;\n readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', string>[];\n readonly createNamespace?: (input: SqlNamespaceTablesInput) => Namespace;\n readonly defaultControlPolicy?: ControlPolicy;\n}\n\n/**\n * Derives the emit output path from the schema input path so artefacts land\n * colocated with the source (e.g. `src/contract/schema.prisma` →\n * `src/contract/contract.json`). The provider owns this because it is the\n * only layer that knows the input path; the upstream `normalizeContractConfig`\n * default is a last-resort fallback for providers that don't carry one.\n */\nfunction defaultOutputFromSchemaPath(schemaPath: string): string {\n const ext = extname(schemaPath);\n if (ext.length === 0) return `${schemaPath}.json`;\n const base = schemaPath.slice(0, -ext.length);\n // PSL schemas commonly use `schema.prisma`; the emitted JSON is called\n // `contract.json` to mirror the rest of the toolchain, not `schema.json`.\n // Match only the exact basename `schema` so files like `my-schema.prisma`\n // are not silently rewritten to `my-contract.json`.\n if (basename(base) === 'schema') {\n return `${base.slice(0, -'schema'.length)}contract.json`;\n }\n return `${base}.json`;\n}\n\nfunction buildColumnDescriptorMap(\n scalarTypeDescriptors: ReadonlyMap<string, string>,\n codecLookup: CodecLookup,\n): ReadonlyMap<string, ColumnDescriptor> {\n const result = new Map<string, ColumnDescriptor>();\n for (const [typeName, codecId] of scalarTypeDescriptors) {\n const nativeType = codecLookup.targetTypesFor(codecId)?.[0];\n if (nativeType === undefined) continue;\n result.set(typeName, { codecId, nativeType });\n }\n return result;\n}\n\nexport function prismaContract(schemaPath: string, options: PrismaContractOptions): ContractConfig {\n return {\n source: {\n inputs: [schemaPath],\n load: async (context) => {\n const [absoluteSchemaPath] = context.resolvedInputs;\n if (absoluteSchemaPath === undefined) {\n throw new Error(\n 'prismaContract: context.resolvedInputs is empty. The CLI config loader should populate it positional-matched with source.inputs.',\n );\n }\n let schema: string;\n try {\n schema = await readFile(absoluteSchemaPath, 'utf-8');\n } catch (error) {\n const message = String(error);\n return notOk({\n summary: `Failed to read Prisma schema at \"${schemaPath}\"`,\n diagnostics: [\n {\n code: 'PSL_SCHEMA_READ_FAILED',\n message,\n sourceId: schemaPath,\n },\n ],\n meta: { schemaPath, absoluteSchemaPath, cause: message },\n });\n }\n\n const document = parsePslDocument({\n schema,\n sourceId: schemaPath,\n });\n\n const scalarTypeDescriptors = buildColumnDescriptorMap(\n context.scalarTypeDescriptors,\n context.codecLookup,\n );\n\n const interpreted = interpretPslDocumentToSqlContract({\n document,\n target: options.target,\n authoringContributions: context.authoringContributions,\n scalarTypeDescriptors,\n ...ifDefined(\n 'composedExtensionPacks',\n context.composedExtensionPacks.length > 0\n ? [...context.composedExtensionPacks]\n : undefined,\n ),\n ...ifDefined(\n 'composedExtensionPackRefs',\n options.composedExtensionPackRefs?.length\n ? options.composedExtensionPackRefs\n : undefined,\n ),\n controlMutationDefaults: context.controlMutationDefaults,\n ...ifDefined('createNamespace', options.createNamespace),\n });\n if (!interpreted.ok) {\n return interpreted;\n }\n\n return ok(\n applySpecifierDefaultControlPolicy(interpreted.value, options.defaultControlPolicy),\n );\n },\n },\n output: options.output ?? defaultOutputFromSchemaPath(schemaPath),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AA8BA,SAAS,4BAA4B,YAA4B;CAC/D,MAAM,MAAM,QAAQ,UAAU;CAC9B,IAAI,IAAI,WAAW,GAAG,OAAO,GAAG,WAAW;CAC3C,MAAM,OAAO,WAAW,MAAM,GAAG,CAAC,IAAI,MAAM;CAK5C,IAAI,SAAS,IAAI,MAAM,UACrB,OAAO,GAAG,KAAK,MAAM,GAAG,EAAgB,EAAE;CAE5C,OAAO,GAAG,KAAK;AACjB;AAEA,SAAS,yBACP,uBACA,aACuC;CACvC,MAAM,yBAAS,IAAI,IAA8B;CACjD,KAAK,MAAM,CAAC,UAAU,YAAY,uBAAuB;EACvD,MAAM,aAAa,YAAY,eAAe,OAAO,IAAI;EACzD,IAAI,eAAe,KAAA,GAAW;EAC9B,OAAO,IAAI,UAAU;GAAE;GAAS;EAAW,CAAC;CAC9C;CACA,OAAO;AACT;AAEA,SAAgB,eAAe,YAAoB,SAAgD;CACjG,OAAO;EACL,QAAQ;GACN,QAAQ,CAAC,UAAU;GACnB,MAAM,OAAO,YAAY;IACvB,MAAM,CAAC,sBAAsB,QAAQ;IACrC,IAAI,uBAAuB,KAAA,GACzB,MAAM,IAAI,MACR,kIACF;IAEF,IAAI;IACJ,IAAI;KACF,SAAS,MAAM,SAAS,oBAAoB,OAAO;IACrD,SAAS,OAAO;KACd,MAAM,UAAU,OAAO,KAAK;KAC5B,OAAO,MAAM;MACX,SAAS,oCAAoC,WAAW;MACxD,aAAa,CACX;OACE,MAAM;OACN;OACA,UAAU;MACZ,CACF;MACA,MAAM;OAAE;OAAY;OAAoB,OAAO;MAAQ;KACzD,CAAC;IACH;IAEA,MAAM,WAAW,iBAAiB;KAChC;KACA,UAAU;IACZ,CAAC;IAED,MAAM,wBAAwB,yBAC5B,QAAQ,uBACR,QAAQ,WACV;IAEA,MAAM,cAAc,kCAAkC;KACpD;KACA,QAAQ,QAAQ;KAChB,wBAAwB,QAAQ;KAChC;KACA,GAAG,UACD,0BACA,QAAQ,uBAAuB,SAAS,IACpC,CAAC,GAAG,QAAQ,sBAAsB,IAClC,KAAA,CACN;KACA,GAAG,UACD,6BACA,QAAQ,2BAA2B,SAC/B,QAAQ,4BACR,KAAA,CACN;KACA,yBAAyB,QAAQ;KACjC,GAAG,UAAU,mBAAmB,QAAQ,eAAe;IACzD,CAAC;IACD,IAAI,CAAC,YAAY,IACf,OAAO;IAGT,OAAO,GACL,mCAAmC,YAAY,OAAO,QAAQ,oBAAoB,CACpF;GACF;EACF;EACA,QAAQ,QAAQ,UAAU,4BAA4B,UAAU;CAClE;AACF"}
|
package/package.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-contract-psl",
|
|
3
|
-
"version": "0.12.0-dev.
|
|
3
|
+
"version": "0.12.0-dev.50",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "PSL-to-SQL ContractIR interpreter for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/config": "0.12.0-dev.
|
|
10
|
-
"@prisma-next/contract": "0.12.0-dev.
|
|
11
|
-
"@prisma-next/framework-components": "0.12.0-dev.
|
|
12
|
-
"@prisma-next/psl-parser": "0.12.0-dev.
|
|
13
|
-
"@prisma-next/sql-contract": "0.12.0-dev.
|
|
14
|
-
"@prisma-next/sql-contract-ts": "0.12.0-dev.
|
|
15
|
-
"@prisma-next/utils": "0.12.0-dev.
|
|
9
|
+
"@prisma-next/config": "0.12.0-dev.50",
|
|
10
|
+
"@prisma-next/contract": "0.12.0-dev.50",
|
|
11
|
+
"@prisma-next/framework-components": "0.12.0-dev.50",
|
|
12
|
+
"@prisma-next/psl-parser": "0.12.0-dev.50",
|
|
13
|
+
"@prisma-next/sql-contract": "0.12.0-dev.50",
|
|
14
|
+
"@prisma-next/sql-contract-ts": "0.12.0-dev.50",
|
|
15
|
+
"@prisma-next/utils": "0.12.0-dev.50",
|
|
16
16
|
"pathe": "^2.0.3"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@prisma-next/contract-authoring": "0.12.0-dev.
|
|
20
|
-
"@prisma-next/test-utils": "0.12.0-dev.
|
|
21
|
-
"@prisma-next/tsconfig": "0.12.0-dev.
|
|
22
|
-
"@prisma-next/tsdown": "0.12.0-dev.
|
|
19
|
+
"@prisma-next/contract-authoring": "0.12.0-dev.50",
|
|
20
|
+
"@prisma-next/test-utils": "0.12.0-dev.50",
|
|
21
|
+
"@prisma-next/tsconfig": "0.12.0-dev.50",
|
|
22
|
+
"@prisma-next/tsdown": "0.12.0-dev.50",
|
|
23
23
|
"arktype": "^2.2.0",
|
|
24
24
|
"tsdown": "0.22.0",
|
|
25
25
|
"typescript": "5.9.3",
|
package/src/interpreter.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ContractField,
|
|
9
9
|
ContractModel,
|
|
10
10
|
ContractValueObject,
|
|
11
|
+
ControlPolicy,
|
|
11
12
|
} from '@prisma-next/contract/types';
|
|
12
13
|
import { crossRef } from '@prisma-next/contract/types';
|
|
13
14
|
import type {
|
|
@@ -23,7 +24,6 @@ import type {
|
|
|
23
24
|
MutationDefaultGeneratorDescriptor,
|
|
24
25
|
} from '@prisma-next/framework-components/control';
|
|
25
26
|
import type { Namespace } from '@prisma-next/framework-components/ir';
|
|
26
|
-
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
27
27
|
import type {
|
|
28
28
|
ParsePslDocumentResult,
|
|
29
29
|
PslAttribute,
|
|
@@ -37,17 +37,20 @@ import type {
|
|
|
37
37
|
import {
|
|
38
38
|
isPostgresEnumStorageEntry,
|
|
39
39
|
type PostgresEnumStorageEntry,
|
|
40
|
+
type SqlModelStorage,
|
|
40
41
|
type SqlNamespaceTablesInput,
|
|
41
42
|
type StorageTypeInstance,
|
|
42
43
|
} from '@prisma-next/sql-contract/types';
|
|
43
44
|
import {
|
|
44
45
|
buildSqlContractFromDefinition,
|
|
46
|
+
type FieldNode,
|
|
45
47
|
type ForeignKeyNode,
|
|
46
48
|
type IndexNode,
|
|
47
49
|
type ModelNode,
|
|
48
50
|
type PrimaryKeyNode,
|
|
49
51
|
type UniqueConstraintNode,
|
|
50
52
|
} from '@prisma-next/sql-contract-ts/contract-builder';
|
|
53
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
51
54
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
52
55
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
53
56
|
import {
|
|
@@ -58,6 +61,7 @@ import {
|
|
|
58
61
|
mapFieldNamesToColumns,
|
|
59
62
|
parseAttributeFieldList,
|
|
60
63
|
parseConstraintMapArgument,
|
|
64
|
+
parseControlPolicyAttribute,
|
|
61
65
|
parseMapName,
|
|
62
66
|
parseObjectLiteralStringMap,
|
|
63
67
|
parseQuotedStringLiteral,
|
|
@@ -229,10 +233,6 @@ const UNSPECIFIED_PSL_NAMESPACE_NAME = '__unspecified__';
|
|
|
229
233
|
* slot empty (which means the late-bound default at the `StorageTable`
|
|
230
234
|
* layer; emitted JSON omits the field).
|
|
231
235
|
*/
|
|
232
|
-
function defaultSqlNamespaceIdForTarget(targetId: string): string {
|
|
233
|
-
return targetId === 'postgres' ? 'public' : UNBOUND_NAMESPACE_ID;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
236
|
function resolveNamespaceIdForSqlTarget(input: {
|
|
237
237
|
readonly bucketName: string;
|
|
238
238
|
readonly targetId: string;
|
|
@@ -647,6 +647,8 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
|
|
|
647
647
|
: undefined;
|
|
648
648
|
const hasInlinePrimaryKey = primaryKey !== undefined;
|
|
649
649
|
let blockPrimaryKeyDeclared = false;
|
|
650
|
+
let controlPolicyDeclared = false;
|
|
651
|
+
let controlPolicy: ControlPolicy | undefined;
|
|
650
652
|
|
|
651
653
|
const resultBackrelationCandidates: ModelBackrelationCandidate[] = [];
|
|
652
654
|
for (const field of model.fields) {
|
|
@@ -733,6 +735,27 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
|
|
|
733
735
|
if (modelAttribute.name === 'discriminator' || modelAttribute.name === 'base') {
|
|
734
736
|
continue;
|
|
735
737
|
}
|
|
738
|
+
if (modelAttribute.name === 'control') {
|
|
739
|
+
if (controlPolicyDeclared) {
|
|
740
|
+
diagnostics.push({
|
|
741
|
+
code: 'PSL_DUPLICATE_ATTRIBUTE',
|
|
742
|
+
message: `\`@@control\` declared more than once on model "${model.name}".`,
|
|
743
|
+
sourceId,
|
|
744
|
+
span: modelAttribute.span,
|
|
745
|
+
});
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
controlPolicyDeclared = true;
|
|
749
|
+
const parsed = parseControlPolicyAttribute({
|
|
750
|
+
attribute: modelAttribute,
|
|
751
|
+
sourceId,
|
|
752
|
+
diagnostics,
|
|
753
|
+
});
|
|
754
|
+
if (parsed !== undefined) {
|
|
755
|
+
controlPolicy = parsed;
|
|
756
|
+
}
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
736
759
|
const attributeLabel = `Model "${model.name}" @@${modelAttribute.name}`;
|
|
737
760
|
if (modelAttribute.name === 'id') {
|
|
738
761
|
if (blockPrimaryKeyDeclared) {
|
|
@@ -1103,6 +1126,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
|
|
|
1103
1126
|
...(uniqueConstraints.length > 0 ? { uniques: uniqueConstraints } : {}),
|
|
1104
1127
|
...(indexNodes.length > 0 ? { indexes: indexNodes } : {}),
|
|
1105
1128
|
...(foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {}),
|
|
1129
|
+
...ifDefined('control', controlPolicy),
|
|
1106
1130
|
},
|
|
1107
1131
|
fkRelationMetadata: resultFkRelationMetadata,
|
|
1108
1132
|
backrelationCandidates: resultBackrelationCandidates,
|
|
@@ -1310,12 +1334,27 @@ function resolvePolymorphism(
|
|
|
1310
1334
|
modelNames: Set<string>,
|
|
1311
1335
|
modelMappings: ReadonlyMap<string, ModelNameMapping>,
|
|
1312
1336
|
modelNamespaceIds: ReadonlyMap<string, string>,
|
|
1313
|
-
|
|
1337
|
+
defaultNamespaceId: string,
|
|
1338
|
+
syntheticPkFieldsByVariant: ReadonlyMap<string, readonly string[]>,
|
|
1339
|
+
stiBaseFieldsByBase: ReadonlyMap<string, readonly string[]>,
|
|
1314
1340
|
sourceId: string,
|
|
1315
1341
|
diagnostics: ContractSourceDiagnostic[],
|
|
1316
1342
|
): Record<string, ContractModel> {
|
|
1317
1343
|
let patched = models;
|
|
1318
1344
|
|
|
1345
|
+
// STI variant columns were materialised onto the base storage table so the
|
|
1346
|
+
// variants' `storage.fields` resolve. They are storage-only on the base — the
|
|
1347
|
+
// domain field belongs to the variant — so strip them from the base model's
|
|
1348
|
+
// domain + storage field maps (the table column, built upstream, stays).
|
|
1349
|
+
for (const [baseName, fieldNames] of stiBaseFieldsByBase) {
|
|
1350
|
+
const baseModel = patched[baseName];
|
|
1351
|
+
if (!baseModel || fieldNames.length === 0) continue;
|
|
1352
|
+
patched = {
|
|
1353
|
+
...patched,
|
|
1354
|
+
[baseName]: stripStorageOnlyDomainFields(baseModel, fieldNames),
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1319
1358
|
for (const [modelName, decl] of discriminatorDeclarations) {
|
|
1320
1359
|
if (baseDeclarations.has(modelName)) {
|
|
1321
1360
|
diagnostics.push({
|
|
@@ -1410,22 +1449,211 @@ function resolvePolymorphism(
|
|
|
1410
1449
|
variantMapping?.model.attributes.some((attr) => attr.name === 'map') ?? false;
|
|
1411
1450
|
const resolvedTable = hasExplicitMap ? variantMapping?.tableName : baseMapping?.tableName;
|
|
1412
1451
|
|
|
1452
|
+
const patchedVariant: ContractModel = {
|
|
1453
|
+
...variantModel,
|
|
1454
|
+
base: crossRef(
|
|
1455
|
+
baseDecl.baseName,
|
|
1456
|
+
modelNamespaceIds.get(baseDecl.baseName) ?? defaultNamespaceId,
|
|
1457
|
+
),
|
|
1458
|
+
...(resolvedTable ? { storage: { ...variantModel.storage, table: resolvedTable } } : {}),
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1413
1461
|
patched = {
|
|
1414
1462
|
...patched,
|
|
1415
|
-
[variantName]:
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
modelNamespaceIds.get(baseDecl.baseName) ?? defaultSqlNamespaceIdForTarget(targetId),
|
|
1420
|
-
),
|
|
1421
|
-
...(resolvedTable ? { storage: { ...variantModel.storage, table: resolvedTable } } : {}),
|
|
1422
|
-
},
|
|
1463
|
+
[variantName]: stripStorageOnlyDomainFields(
|
|
1464
|
+
patchedVariant,
|
|
1465
|
+
syntheticPkFieldsByVariant.get(variantName) ?? [],
|
|
1466
|
+
),
|
|
1423
1467
|
};
|
|
1424
1468
|
}
|
|
1425
1469
|
|
|
1426
1470
|
return patched;
|
|
1427
1471
|
}
|
|
1428
1472
|
|
|
1473
|
+
/**
|
|
1474
|
+
* Multi-table-inheritance variants (`@@base` + their own `@@map`) live in a
|
|
1475
|
+
* separate table from their base. The ORM joins that table to the base on the
|
|
1476
|
+
* shared primary key (`base.id = variant.id`), so the variant storage table
|
|
1477
|
+
* must carry the base PK column even though the variant domain model declares
|
|
1478
|
+
* only its own fields. This enriches each MTI variant's `ModelNode` with that
|
|
1479
|
+
* link column, a primary key on it, and a FK back to the base table.
|
|
1480
|
+
*
|
|
1481
|
+
* The link column is reported back per variant in `syntheticPkFieldsByVariant`
|
|
1482
|
+
* so the domain-model patch can drop it again — keeping the variant's domain
|
|
1483
|
+
* surface thin (its create/read inputs don't gain a redundant `id`) while the
|
|
1484
|
+
* storage table stays joinable. Single-table-inheritance variants (no own
|
|
1485
|
+
* table) are left untouched.
|
|
1486
|
+
*/
|
|
1487
|
+
function materializeMtiVariantStorageLinks(
|
|
1488
|
+
modelNodes: readonly ModelNode[],
|
|
1489
|
+
baseDeclarations: ReadonlyMap<string, BaseDeclaration>,
|
|
1490
|
+
stiVariantNames: ReadonlySet<string>,
|
|
1491
|
+
): { modelNodes: ModelNode[]; syntheticPkFieldsByVariant: Map<string, readonly string[]> } {
|
|
1492
|
+
const nodeByModel = new Map(modelNodes.map((node) => [node.modelName, node]));
|
|
1493
|
+
const syntheticPkFieldsByVariant = new Map<string, readonly string[]>();
|
|
1494
|
+
|
|
1495
|
+
const enriched = modelNodes.map((node): ModelNode => {
|
|
1496
|
+
const baseDecl = baseDeclarations.get(node.modelName);
|
|
1497
|
+
if (!baseDecl) return node;
|
|
1498
|
+
const baseNode = nodeByModel.get(baseDecl.baseName);
|
|
1499
|
+
if (!baseNode) return node;
|
|
1500
|
+
// Single-table inheritance (no own `@@map`) shares the base table; it gets
|
|
1501
|
+
// its columns materialised onto the base instead (see
|
|
1502
|
+
// {@link materializeStiVariantStorageColumns}), never a link column.
|
|
1503
|
+
if (stiVariantNames.has(node.modelName)) return node;
|
|
1504
|
+
const basePrimaryKey = baseNode.id;
|
|
1505
|
+
if (!basePrimaryKey || basePrimaryKey.columns.length === 0) return node;
|
|
1506
|
+
|
|
1507
|
+
const existingColumns = new Set(node.fields.map((field) => field.columnName));
|
|
1508
|
+
const linkFields: FieldNode[] = [];
|
|
1509
|
+
for (const pkColumn of basePrimaryKey.columns) {
|
|
1510
|
+
if (existingColumns.has(pkColumn)) continue;
|
|
1511
|
+
const baseField = baseNode.fields.find(
|
|
1512
|
+
(field): field is FieldNode => 'descriptor' in field && field.columnName === pkColumn,
|
|
1513
|
+
);
|
|
1514
|
+
if (!baseField) continue;
|
|
1515
|
+
linkFields.push({
|
|
1516
|
+
fieldName: baseField.fieldName,
|
|
1517
|
+
columnName: pkColumn,
|
|
1518
|
+
descriptor: baseField.descriptor,
|
|
1519
|
+
nullable: false,
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
if (linkFields.length === 0) return node;
|
|
1523
|
+
|
|
1524
|
+
syntheticPkFieldsByVariant.set(
|
|
1525
|
+
node.modelName,
|
|
1526
|
+
linkFields.map((field) => field.fieldName),
|
|
1527
|
+
);
|
|
1528
|
+
|
|
1529
|
+
const foreignKey: ForeignKeyNode = {
|
|
1530
|
+
columns: basePrimaryKey.columns,
|
|
1531
|
+
references: {
|
|
1532
|
+
model: baseNode.modelName,
|
|
1533
|
+
table: baseNode.tableName,
|
|
1534
|
+
columns: basePrimaryKey.columns,
|
|
1535
|
+
...ifDefined('namespaceId', baseNode.namespaceId),
|
|
1536
|
+
},
|
|
1537
|
+
constraint: true,
|
|
1538
|
+
// The link columns are the variant's own primary key, which already
|
|
1539
|
+
// carries a unique index — a separate FK backing index would be redundant.
|
|
1540
|
+
index: false,
|
|
1541
|
+
// Deleting a base row must delete its variant extension row — classic
|
|
1542
|
+
// multi-table-inheritance semantics.
|
|
1543
|
+
onDelete: 'cascade',
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
return {
|
|
1547
|
+
...node,
|
|
1548
|
+
fields: [...linkFields, ...node.fields],
|
|
1549
|
+
id: { columns: basePrimaryKey.columns },
|
|
1550
|
+
foreignKeys: [...(node.foreignKeys ?? []), foreignKey],
|
|
1551
|
+
};
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
return { modelNodes: enriched, syntheticPkFieldsByVariant };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Single-table-inheritance variants (`@@base` with no own `@@map`) share the
|
|
1559
|
+
* base table: `resolvePolymorphism` points the variant's `storage.table` at the
|
|
1560
|
+
* base, and the ORM reads variant-declared fields straight off the base table.
|
|
1561
|
+
* For that to validate and round-trip, the base storage table must physically
|
|
1562
|
+
* carry every STI variant's declared columns. This enriches the base
|
|
1563
|
+
* `ModelNode` with those columns.
|
|
1564
|
+
*
|
|
1565
|
+
* The materialised columns are always nullable in storage: the base table hosts
|
|
1566
|
+
* every variant's rows, so a column a variant declares as required is still
|
|
1567
|
+
* NULL on sibling-variant rows. The variant's domain field keeps its declared
|
|
1568
|
+
* nullability — required-in-domain / nullable-in-storage is the intended STI
|
|
1569
|
+
* shape.
|
|
1570
|
+
*
|
|
1571
|
+
* Collisions (two variants declaring the same column, or a variant column name
|
|
1572
|
+
* clashing with a base column) are resolved skip-if-exists here, mirroring the
|
|
1573
|
+
* MTI link guard; surfacing them as diagnostics is tracked separately
|
|
1574
|
+
* (TML-2827).
|
|
1575
|
+
*/
|
|
1576
|
+
function materializeStiVariantStorageColumns(
|
|
1577
|
+
modelNodes: readonly ModelNode[],
|
|
1578
|
+
baseDeclarations: ReadonlyMap<string, BaseDeclaration>,
|
|
1579
|
+
stiVariantNames: ReadonlySet<string>,
|
|
1580
|
+
): { modelNodes: ModelNode[]; stiBaseFieldsByBase: Map<string, readonly string[]> } {
|
|
1581
|
+
if (stiVariantNames.size === 0) {
|
|
1582
|
+
return { modelNodes: [...modelNodes], stiBaseFieldsByBase: new Map() };
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
const nodeByModel = new Map(modelNodes.map((node) => [node.modelName, node]));
|
|
1586
|
+
type StiColumn = ModelNode['fields'][number];
|
|
1587
|
+
const stiColumnsByBase = new Map<string, StiColumn[]>();
|
|
1588
|
+
|
|
1589
|
+
for (const variantName of stiVariantNames) {
|
|
1590
|
+
const variantNode = nodeByModel.get(variantName);
|
|
1591
|
+
const baseDecl = baseDeclarations.get(variantName);
|
|
1592
|
+
if (!variantNode || !baseDecl) continue;
|
|
1593
|
+
const baseNode = nodeByModel.get(baseDecl.baseName);
|
|
1594
|
+
if (!baseNode) continue;
|
|
1595
|
+
|
|
1596
|
+
const baseColumns = new Set(baseNode.fields.map((field) => field.columnName));
|
|
1597
|
+
const claimed = stiColumnsByBase.get(baseDecl.baseName) ?? [];
|
|
1598
|
+
const claimedColumns = new Set(claimed.map((field) => field.columnName));
|
|
1599
|
+
|
|
1600
|
+
for (const field of variantNode.fields) {
|
|
1601
|
+
if (baseColumns.has(field.columnName) || claimedColumns.has(field.columnName)) {
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
claimedColumns.add(field.columnName);
|
|
1605
|
+
claimed.push({ ...field, nullable: true });
|
|
1606
|
+
}
|
|
1607
|
+
stiColumnsByBase.set(baseDecl.baseName, claimed);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// The materialised columns exist on the base STORAGE table so the variants'
|
|
1611
|
+
// `storage.fields` resolve, but they are NOT base DOMAIN fields — `severity`
|
|
1612
|
+
// belongs to `Bug`, not to `Task`. Report the materialised field names per
|
|
1613
|
+
// base so the domain patch can strip them from the base model (the table
|
|
1614
|
+
// column stays); this is the STI analogue of `syntheticPkFieldsByVariant`.
|
|
1615
|
+
const stiBaseFieldsByBase = new Map<string, readonly string[]>();
|
|
1616
|
+
for (const [baseName, columns] of stiColumnsByBase) {
|
|
1617
|
+
stiBaseFieldsByBase.set(
|
|
1618
|
+
baseName,
|
|
1619
|
+
columns.map((field) => field.fieldName),
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
const enriched = modelNodes.map((node): ModelNode => {
|
|
1624
|
+
// STI variant: contributes a domain model but no storage table of its own.
|
|
1625
|
+
if (stiVariantNames.has(node.modelName)) {
|
|
1626
|
+
return { ...node, sharesBaseTable: true };
|
|
1627
|
+
}
|
|
1628
|
+
const stiColumns = stiColumnsByBase.get(node.modelName);
|
|
1629
|
+
if (!stiColumns || stiColumns.length === 0) return node;
|
|
1630
|
+
return { ...node, fields: [...node.fields, ...stiColumns] };
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
return { modelNodes: enriched, stiBaseFieldsByBase };
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Drop the storage-only link fields (added by
|
|
1638
|
+
* {@link materializeMtiVariantStorageLinks}) from a variant's domain model, so
|
|
1639
|
+
* the domain surface stays thin while the storage table keeps the link column.
|
|
1640
|
+
*/
|
|
1641
|
+
function stripStorageOnlyDomainFields(
|
|
1642
|
+
model: ContractModel,
|
|
1643
|
+
fieldNames: readonly string[],
|
|
1644
|
+
): ContractModel {
|
|
1645
|
+
if (fieldNames.length === 0) return model;
|
|
1646
|
+
const fields = { ...model.fields };
|
|
1647
|
+
for (const name of fieldNames) delete fields[name];
|
|
1648
|
+
const storage = blindCast<
|
|
1649
|
+
SqlModelStorage,
|
|
1650
|
+
'SQL interpreter domain models always carry SqlModelStorage'
|
|
1651
|
+
>(model.storage);
|
|
1652
|
+
const storageFields = { ...storage.fields };
|
|
1653
|
+
for (const name of fieldNames) delete storageFields[name];
|
|
1654
|
+
return { ...model, fields, storage: { ...storage, fields: storageFields } };
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1429
1657
|
export function interpretPslDocumentToSqlContract(
|
|
1430
1658
|
input: InterpretPslDocumentToSqlContractInput,
|
|
1431
1659
|
): Result<Contract, ContractSourceDiagnostics> {
|
|
@@ -1632,6 +1860,27 @@ export function interpretPslDocumentToSqlContract(
|
|
|
1632
1860
|
diagnostics,
|
|
1633
1861
|
);
|
|
1634
1862
|
|
|
1863
|
+
// A variant with `@@base` but no own `@@map` is single-table inheritance:
|
|
1864
|
+
// it shares the base table. (`@@map` ⇒ multi-table inheritance.) This is the
|
|
1865
|
+
// authoritative STI/MTI signal — the variant's resolved table name is not,
|
|
1866
|
+
// because a no-`@@map` STI variant still gets a `lowerFirst(name)` default
|
|
1867
|
+
// table name that differs from the base before `resolvePolymorphism` rewrites
|
|
1868
|
+
// it onto the base table.
|
|
1869
|
+
const stiVariantNames = new Set<string>();
|
|
1870
|
+
for (const variantName of baseDeclarations.keys()) {
|
|
1871
|
+
const variantMapping = modelMappings.get(variantName);
|
|
1872
|
+
const hasExplicitMap =
|
|
1873
|
+
variantMapping?.model.attributes.some((attr) => attr.name === 'map') ?? false;
|
|
1874
|
+
if (!hasExplicitMap) {
|
|
1875
|
+
stiVariantNames.add(variantName);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
const { modelNodes: mtiLinkedModelNodes, syntheticPkFieldsByVariant } =
|
|
1880
|
+
materializeMtiVariantStorageLinks(modelNodes, baseDeclarations, stiVariantNames);
|
|
1881
|
+
const { modelNodes: stiColumnModelNodes, stiBaseFieldsByBase } =
|
|
1882
|
+
materializeStiVariantStorageColumns(mtiLinkedModelNodes, baseDeclarations, stiVariantNames);
|
|
1883
|
+
|
|
1635
1884
|
const valueObjects = buildValueObjects({
|
|
1636
1885
|
compositeTypes,
|
|
1637
1886
|
enumTypeDescriptors: allEnumTypeDescriptors,
|
|
@@ -1667,7 +1916,7 @@ export function interpretPslDocumentToSqlContract(
|
|
|
1667
1916
|
? { namespaceTypes: namespaceEnumStorageTypes }
|
|
1668
1917
|
: {}),
|
|
1669
1918
|
...ifDefined('createNamespace', input.createNamespace),
|
|
1670
|
-
models:
|
|
1919
|
+
models: stiColumnModelNodes.map((model) => ({
|
|
1671
1920
|
...model,
|
|
1672
1921
|
...(modelRelations.has(model.modelName)
|
|
1673
1922
|
? {
|
|
@@ -1687,7 +1936,7 @@ export function interpretPslDocumentToSqlContract(
|
|
|
1687
1936
|
`duplicate model name "${modelName}" across domain namespaces during PSL interpretation`,
|
|
1688
1937
|
);
|
|
1689
1938
|
}
|
|
1690
|
-
modelsForPatch[modelName] = model
|
|
1939
|
+
modelsForPatch[modelName] = model;
|
|
1691
1940
|
}
|
|
1692
1941
|
}
|
|
1693
1942
|
let patchedModels = patchModelDomainFields(modelsForPatch, modelResolvedFields);
|
|
@@ -1700,7 +1949,9 @@ export function interpretPslDocumentToSqlContract(
|
|
|
1700
1949
|
modelNames,
|
|
1701
1950
|
modelMappings,
|
|
1702
1951
|
modelNamespaceIds,
|
|
1703
|
-
input.target.
|
|
1952
|
+
input.target.defaultNamespaceId,
|
|
1953
|
+
syntheticPkFieldsByVariant,
|
|
1954
|
+
stiBaseFieldsByBase,
|
|
1704
1955
|
sourceId,
|
|
1705
1956
|
polyDiagnostics,
|
|
1706
1957
|
);
|
|
@@ -1736,7 +1987,7 @@ export function interpretPslDocumentToSqlContract(
|
|
|
1736
1987
|
...(namespaceSlice.valueObjects !== undefined
|
|
1737
1988
|
? { valueObjects: namespaceSlice.valueObjects }
|
|
1738
1989
|
: {}),
|
|
1739
|
-
...(namespaceId ===
|
|
1990
|
+
...(namespaceId === input.target.defaultNamespaceId &&
|
|
1740
1991
|
Object.keys(valueObjects).length > 0
|
|
1741
1992
|
? { valueObjects }
|
|
1742
1993
|
: {}),
|
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
|
/**
|
|
@@ -106,12 +112,15 @@ export function prismaContract(schemaPath: string, options: PrismaContractOption
|
|
|
106
112
|
: undefined,
|
|
107
113
|
),
|
|
108
114
|
controlMutationDefaults: context.controlMutationDefaults,
|
|
115
|
+
...ifDefined('createNamespace', options.createNamespace),
|
|
109
116
|
});
|
|
110
117
|
if (!interpreted.ok) {
|
|
111
118
|
return interpreted;
|
|
112
119
|
}
|
|
113
120
|
|
|
114
|
-
return ok(
|
|
121
|
+
return ok(
|
|
122
|
+
applySpecifierDefaultControlPolicy(interpreted.value, options.defaultControlPolicy),
|
|
123
|
+
);
|
|
115
124
|
},
|
|
116
125
|
},
|
|
117
126
|
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;
|