@prisma-next/framework-components 0.12.0-dev.7 → 0.12.0-dev.70
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/dist/authoring.d.mts +2 -2
- package/dist/authoring.mjs +2 -2
- package/dist/{codec-BFOsuHKK.d.mts → codec-DCQAerzB.d.mts} +1 -1
- package/dist/{codec-BFOsuHKK.d.mts.map → codec-DCQAerzB.d.mts.map} +1 -1
- package/dist/codec.d.mts +1 -1
- package/dist/codec.d.mts.map +1 -1
- package/dist/components.d.mts +1 -1
- package/dist/components.mjs +1 -1
- package/dist/components.mjs.map +1 -1
- package/dist/control.d.mts +85 -13
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +89 -7
- package/dist/control.mjs.map +1 -1
- package/dist/{emission-types-CMv_053d.d.mts → emission-types-vfpSTe63.d.mts} +2 -2
- package/dist/{emission-types-CMv_053d.d.mts.map → emission-types-vfpSTe63.d.mts.map} +1 -1
- package/dist/emission.d.mts +3 -3
- package/dist/execution.d.mts +1 -1
- package/dist/execution.d.mts.map +1 -1
- package/dist/execution.mjs +1 -1
- package/dist/{framework-authoring-DcEZ5Lin.mjs → framework-authoring-CnwPJCO4.mjs} +76 -5
- package/dist/framework-authoring-CnwPJCO4.mjs.map +1 -0
- package/dist/framework-authoring-Cyde8zSN.d.mts +380 -0
- package/dist/framework-authoring-Cyde8zSN.d.mts.map +1 -0
- package/dist/{framework-components-FdqmlGUj.mjs → framework-components-DbCS57go.mjs} +1 -1
- package/dist/{framework-components-FdqmlGUj.mjs.map → framework-components-DbCS57go.mjs.map} +1 -1
- package/dist/{framework-components-CuoUhyB5.d.mts → framework-components-DdqvMc8S.d.mts} +6 -5
- package/dist/{framework-components-CuoUhyB5.d.mts.map → framework-components-DdqvMc8S.d.mts.map} +1 -1
- package/dist/ir.d.mts +20 -18
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +17 -14
- package/dist/ir.mjs.map +1 -1
- package/dist/{psl-ast-BDXL7iCg.d.mts → psl-ast-DRzRF9rS.d.mts} +57 -14
- package/dist/psl-ast-DRzRF9rS.d.mts.map +1 -0
- package/dist/psl-ast.d.mts +37 -2
- package/dist/psl-ast.d.mts.map +1 -0
- package/dist/psl-ast.mjs +142 -1
- package/dist/psl-ast.mjs.map +1 -1
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs.map +1 -1
- package/dist/{types-import-spec-BxI5cSQy.d.mts → types-import-spec-DRKzrJ20.d.mts} +1 -1
- package/dist/{types-import-spec-BxI5cSQy.d.mts.map → types-import-spec-DRKzrJ20.d.mts.map} +1 -1
- package/dist/utils.mjs.map +1 -1
- package/package.json +9 -9
- package/src/control/control-instances.ts +15 -5
- package/src/control/control-migration-types.ts +20 -2
- package/src/control/control-result-types.ts +4 -1
- package/src/control/control-stack.ts +123 -4
- package/src/control/psl-ast.ts +73 -27
- package/src/control/psl-extension-block-validator.ts +340 -0
- package/src/control/verifier-disposition.ts +62 -0
- package/src/exports/authoring.ts +16 -0
- package/src/exports/control.ts +7 -0
- package/src/exports/psl-ast.ts +2 -0
- package/src/ir/namespace.ts +15 -13
- package/src/ir/storage.ts +8 -7
- package/src/shared/framework-authoring.ts +215 -2
- package/src/shared/framework-components.ts +2 -0
- package/src/shared/psl-extension-block.ts +184 -0
- package/dist/framework-authoring-BPPe9C9D.d.mts +0 -183
- package/dist/framework-authoring-BPPe9C9D.d.mts.map +0 -1
- package/dist/framework-authoring-DcEZ5Lin.mjs.map +0 -1
- package/dist/psl-ast-BDXL7iCg.d.mts.map +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
2
|
+
import type { SchemaVerificationNode } from './control-result-types';
|
|
3
|
+
|
|
4
|
+
export type VerificationStatus = SchemaVerificationNode['status'];
|
|
5
|
+
|
|
6
|
+
export type VerifierOutcome = VerificationStatus | 'suppress';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Target-neutral classification of a verifier finding, abstracted away from any
|
|
10
|
+
* one storage model's vocabulary. Each family classifies its own concrete issue
|
|
11
|
+
* kinds into these categories; the framework only grades the category against a
|
|
12
|
+
* control policy.
|
|
13
|
+
*
|
|
14
|
+
* - `declaredMissing` — a declared object/element is absent from the database.
|
|
15
|
+
* - `declaredIncompatible` — a declared object/element exists but its shape diverges.
|
|
16
|
+
* - `valueDrift` — the value set of an existing type drifted (e.g. enum values).
|
|
17
|
+
* - `extraNestedElement` — an undeclared element nested inside a declared object
|
|
18
|
+
* (a SQL column, a document field).
|
|
19
|
+
* - `extraAuxiliary` — an undeclared auxiliary attached to a declared object
|
|
20
|
+
* (a SQL constraint/index, a Mongo index/validator).
|
|
21
|
+
* - `extraTopLevelObject` — an undeclared top-level object (a SQL table, a
|
|
22
|
+
* Mongo collection).
|
|
23
|
+
*/
|
|
24
|
+
export type VerifierIssueCategory =
|
|
25
|
+
| 'declaredMissing'
|
|
26
|
+
| 'declaredIncompatible'
|
|
27
|
+
| 'valueDrift'
|
|
28
|
+
| 'extraNestedElement'
|
|
29
|
+
| 'extraAuxiliary'
|
|
30
|
+
| 'extraTopLevelObject';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Grades a target-neutral issue category against a control policy.
|
|
34
|
+
*
|
|
35
|
+
* - `observed` warns on everything.
|
|
36
|
+
* - `tolerated` suppresses only an extra nested element (everything else fails).
|
|
37
|
+
* - `external` suppresses every extra category and value drift (existence and
|
|
38
|
+
* declared-shape divergences still fail).
|
|
39
|
+
* - `managed` (and any other) fails.
|
|
40
|
+
*/
|
|
41
|
+
export function dispositionForCategory(
|
|
42
|
+
controlPolicy: ControlPolicy,
|
|
43
|
+
category: VerifierIssueCategory,
|
|
44
|
+
): VerifierOutcome {
|
|
45
|
+
if (controlPolicy === 'observed') {
|
|
46
|
+
return 'warn';
|
|
47
|
+
}
|
|
48
|
+
if (controlPolicy === 'tolerated' && category === 'extraNestedElement') {
|
|
49
|
+
return 'suppress';
|
|
50
|
+
}
|
|
51
|
+
if (controlPolicy === 'external') {
|
|
52
|
+
if (
|
|
53
|
+
category === 'extraNestedElement' ||
|
|
54
|
+
category === 'extraAuxiliary' ||
|
|
55
|
+
category === 'extraTopLevelObject' ||
|
|
56
|
+
category === 'valueDrift'
|
|
57
|
+
) {
|
|
58
|
+
return 'suppress';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return 'fail';
|
|
62
|
+
}
|
package/src/exports/authoring.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type {
|
|
|
11
11
|
AuthoringFieldNamespace,
|
|
12
12
|
AuthoringFieldPresetDescriptor,
|
|
13
13
|
AuthoringFieldPresetOutput,
|
|
14
|
+
AuthoringPslBlockDescriptor,
|
|
15
|
+
AuthoringPslBlockDescriptorNamespace,
|
|
14
16
|
AuthoringStorageTypeTemplate,
|
|
15
17
|
AuthoringTemplateValue,
|
|
16
18
|
AuthoringTypeConstructorDescriptor,
|
|
@@ -25,8 +27,22 @@ export {
|
|
|
25
27
|
isAuthoringArgRef,
|
|
26
28
|
isAuthoringEntityTypeDescriptor,
|
|
27
29
|
isAuthoringFieldPresetDescriptor,
|
|
30
|
+
isAuthoringPslBlockDescriptor,
|
|
28
31
|
isAuthoringTypeConstructorDescriptor,
|
|
29
32
|
mergeAuthoringNamespaces,
|
|
30
33
|
resolveAuthoringTemplateValue,
|
|
31
34
|
validateAuthoringHelperArguments,
|
|
32
35
|
} from '../shared/framework-authoring';
|
|
36
|
+
export type {
|
|
37
|
+
PslBlockParam,
|
|
38
|
+
PslBlockParamList,
|
|
39
|
+
PslBlockParamOption,
|
|
40
|
+
PslBlockParamRef,
|
|
41
|
+
PslBlockParamValue,
|
|
42
|
+
PslExtensionBlock,
|
|
43
|
+
PslExtensionBlockParamList,
|
|
44
|
+
PslExtensionBlockParamOption,
|
|
45
|
+
PslExtensionBlockParamRef,
|
|
46
|
+
PslExtensionBlockParamScalarValue,
|
|
47
|
+
PslExtensionBlockParamValue,
|
|
48
|
+
} from '../shared/psl-extension-block';
|
package/src/exports/control.ts
CHANGED
|
@@ -95,6 +95,7 @@ export {
|
|
|
95
95
|
assembleControlMutationDefaults,
|
|
96
96
|
assembleScalarTypeDescriptors,
|
|
97
97
|
assertUniqueCodecOwner,
|
|
98
|
+
buildExtensionLoadOrder,
|
|
98
99
|
createControlStack,
|
|
99
100
|
extractCodecLookup,
|
|
100
101
|
extractCodecTypeImports,
|
|
@@ -106,6 +107,12 @@ export type {
|
|
|
106
107
|
SchemaVerifyOptions,
|
|
107
108
|
SchemaVerifyResult,
|
|
108
109
|
} from '../control/schema-verifier';
|
|
110
|
+
export type {
|
|
111
|
+
VerificationStatus,
|
|
112
|
+
VerifierIssueCategory,
|
|
113
|
+
VerifierOutcome,
|
|
114
|
+
} from '../control/verifier-disposition';
|
|
115
|
+
export { dispositionForCategory } from '../control/verifier-disposition';
|
|
109
116
|
export type {
|
|
110
117
|
ControlMutationDefaultEntry,
|
|
111
118
|
ControlMutationDefaultRegistry,
|
package/src/exports/psl-ast.ts
CHANGED
package/src/ir/namespace.ts
CHANGED
|
@@ -41,28 +41,30 @@ export const UNBOUND_NAMESPACE_ID = '__unbound__' as const;
|
|
|
41
41
|
*
|
|
42
42
|
* The framework promises only the coordinate (`id`) — the named storage
|
|
43
43
|
* entities a namespace contains are family-typed (SQL contributes
|
|
44
|
-
* `
|
|
45
|
-
* own native idiom). Generic consumers walking "all
|
|
46
|
-
* through a family-typed namespace, not the framework
|
|
44
|
+
* `table` / `type`, Mongo contributes `collection`, future families pick
|
|
45
|
+
* their own native idiom under `entries`). Generic consumers walking "all
|
|
46
|
+
* named entries" go through a family-typed namespace, not the framework
|
|
47
|
+
* `Namespace`.
|
|
47
48
|
*
|
|
48
49
|
* Every namespace concretion (e.g. family-built SQL namespaces,
|
|
49
50
|
* `MongoUnboundNamespace`, target-promoted namespaces like
|
|
50
|
-
* `PostgresSchema`) carries exactly: `id` (enumerable string),
|
|
51
|
-
* (
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* on
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* family-specific knowledge.
|
|
51
|
+
* `PostgresSchema`) carries exactly: `id` (enumerable string),
|
|
52
|
+
* `entries` (frozen object holding entity-kind slot maps), and `kind`
|
|
53
|
+
* (non-enumerable string discriminator set via `Object.defineProperty`).
|
|
54
|
+
* Each slot map under `entries` uses a singular essence key (`table`,
|
|
55
|
+
* `type`, `collection`, …) mapping entity names to IR classes. No other
|
|
56
|
+
* own-enumerable data lives on a namespace; non-entity computed data lives
|
|
57
|
+
* on the surrounding storage or contract IR. The framework's
|
|
58
|
+
* `elementCoordinates(storage)` walk relies on this invariant to enumerate
|
|
59
|
+
* entities structurally without family-specific knowledge.
|
|
60
60
|
*/
|
|
61
61
|
export interface Namespace extends IRNode, StorageNamespace {
|
|
62
62
|
readonly kind: string;
|
|
63
|
+
readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export abstract class NamespaceBase extends IRNodeBase implements Namespace {
|
|
66
67
|
abstract readonly id: string;
|
|
68
|
+
abstract readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
67
69
|
abstract override readonly kind: string;
|
|
68
70
|
}
|
package/src/ir/storage.ts
CHANGED
|
@@ -30,18 +30,19 @@ export interface EntityCoordinate {
|
|
|
30
30
|
* value, yielded as {@link EntityCoordinate} tuples with
|
|
31
31
|
* `plane: 'storage'` (the parameter type binds the plane).
|
|
32
32
|
*
|
|
33
|
-
* Iterates each namespace's
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* Iterates each namespace's `entries` slot maps structurally. Skips
|
|
34
|
+
* non-object `entries`; `id` and `kind` are not walked (`kind` is
|
|
35
|
+
* non-enumerable on concretions). For every entity-kind key under
|
|
36
|
+
* `entries` whose value is a non-null object, yields one coordinate per
|
|
37
|
+
* entity name in that map. No family-specific slot vocabulary is required.
|
|
38
38
|
*/
|
|
39
39
|
export function* elementCoordinates(
|
|
40
40
|
storage: Pick<StorageBase, 'namespaces'>,
|
|
41
41
|
): Generator<EntityCoordinate> {
|
|
42
42
|
for (const [namespaceId, ns] of Object.entries(storage.namespaces)) {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const entries = ns.entries;
|
|
44
|
+
if (entries === null || typeof entries !== 'object') continue;
|
|
45
|
+
for (const [entityKind, slot] of Object.entries(entries)) {
|
|
45
46
|
if (slot === null || typeof slot !== 'object') continue;
|
|
46
47
|
for (const entityName of Object.keys(slot)) {
|
|
47
48
|
yield { plane: 'storage', namespaceId, entityKind, entityName };
|
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
isColumnDefaultLiteralInputValue,
|
|
8
8
|
isExecutionMutationDefaultValue,
|
|
9
9
|
} from '@prisma-next/contract/types';
|
|
10
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
10
11
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
11
12
|
import type { Type } from 'arktype';
|
|
13
|
+
import type { PslBlockParam } from './psl-extension-block';
|
|
12
14
|
|
|
13
15
|
export type AuthoringArgRef = {
|
|
14
16
|
readonly kind: 'arg';
|
|
@@ -157,10 +159,55 @@ export type AuthoringEntityTypeNamespace = {
|
|
|
157
159
|
readonly [name: string]: AuthoringEntityTypeDescriptor | AuthoringEntityTypeNamespace;
|
|
158
160
|
};
|
|
159
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Declarative descriptor for an extension-contributed top-level PSL block.
|
|
164
|
+
*
|
|
165
|
+
* An extension registers one of these per keyword it contributes. The
|
|
166
|
+
* framework owns the generic parser, validator, and printer — no
|
|
167
|
+
* parsing or printing code runs from the extension.
|
|
168
|
+
*
|
|
169
|
+
* - `keyword` is the PSL top-level identifier this descriptor claims
|
|
170
|
+
* (`policy_select`, `role`, …).
|
|
171
|
+
* - `discriminator` is the routing key used by the printer dispatch and
|
|
172
|
+
* the `entityTypes` lowering factory lookup. Convention:
|
|
173
|
+
* `<target-or-family>-<kind>` (`postgres-policy-select`).
|
|
174
|
+
* - `name.required` declares whether the block must have a name token
|
|
175
|
+
* after the keyword. Currently always `true` — anonymous blocks are
|
|
176
|
+
* not part of the closed-grammar premise — but the field is explicit
|
|
177
|
+
* so the type can evolve without a breaking change.
|
|
178
|
+
* - `parameters` maps parameter names to their value-kind descriptors
|
|
179
|
+
* (`ref` / `value` / `option` / `list`). The generic parser and
|
|
180
|
+
* validator interpret these; the extension supplies no parser or
|
|
181
|
+
* printer function.
|
|
182
|
+
*/
|
|
183
|
+
export interface AuthoringPslBlockDescriptor {
|
|
184
|
+
readonly kind: 'pslBlock';
|
|
185
|
+
readonly keyword: string;
|
|
186
|
+
readonly discriminator: string;
|
|
187
|
+
readonly name: { readonly required: boolean };
|
|
188
|
+
readonly parameters: Record<string, PslBlockParam>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type AuthoringPslBlockDescriptorNamespace = {
|
|
192
|
+
readonly [name: string]: AuthoringPslBlockDescriptor | AuthoringPslBlockDescriptorNamespace;
|
|
193
|
+
};
|
|
194
|
+
|
|
160
195
|
export interface AuthoringContributions {
|
|
161
196
|
readonly type?: AuthoringTypeNamespace;
|
|
162
197
|
readonly field?: AuthoringFieldNamespace;
|
|
163
198
|
readonly entityTypes?: AuthoringEntityTypeNamespace;
|
|
199
|
+
/**
|
|
200
|
+
* Registry of declarative block descriptors this contribution registers,
|
|
201
|
+
* keyed by arbitrary path segments. Each leaf is an
|
|
202
|
+
* {@link AuthoringPslBlockDescriptor} that claims a PSL top-level keyword.
|
|
203
|
+
* The framework owns the generic parser, validator, and printer; the
|
|
204
|
+
* contribution supplies only these declarative descriptors.
|
|
205
|
+
*
|
|
206
|
+
* Contrast with {@link PslNamespace.extensionBlocks}: that field holds
|
|
207
|
+
* the parsed block nodes in a namespace; this field holds the registry
|
|
208
|
+
* of descriptors that teach the parser how to read those blocks.
|
|
209
|
+
*/
|
|
210
|
+
readonly pslBlockDescriptors?: AuthoringPslBlockDescriptorNamespace;
|
|
164
211
|
}
|
|
165
212
|
|
|
166
213
|
export function isAuthoringArgRef(value: unknown): value is AuthoringArgRef {
|
|
@@ -228,6 +275,42 @@ export function isAuthoringEntityTypeDescriptor(
|
|
|
228
275
|
return typeof factory === 'function' || template !== undefined;
|
|
229
276
|
}
|
|
230
277
|
|
|
278
|
+
export function isAuthoringPslBlockDescriptor(
|
|
279
|
+
value: unknown,
|
|
280
|
+
): value is AuthoringPslBlockDescriptor {
|
|
281
|
+
if (typeof value !== 'object' || value === null) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
const record = blindCast<
|
|
285
|
+
Record<string, unknown>,
|
|
286
|
+
'type-guard probing an unknown candidate-descriptor object for known property names'
|
|
287
|
+
>(value);
|
|
288
|
+
if (record['kind'] !== 'pslBlock') {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
const keyword = record['keyword'];
|
|
292
|
+
if (typeof keyword !== 'string' || keyword.length === 0) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
const discriminator = record['discriminator'];
|
|
296
|
+
if (typeof discriminator !== 'string' || discriminator.length === 0) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
const name = record['name'];
|
|
300
|
+
if (typeof name !== 'object' || name === null) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const nameRecord = blindCast<
|
|
304
|
+
Record<string, unknown>,
|
|
305
|
+
'type-guard probing the name property of a candidate pslBlock descriptor'
|
|
306
|
+
>(name);
|
|
307
|
+
if (typeof nameRecord['required'] !== 'boolean') {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const parameters = record['parameters'];
|
|
311
|
+
return typeof parameters === 'object' && parameters !== null && !Array.isArray(parameters);
|
|
312
|
+
}
|
|
313
|
+
|
|
231
314
|
/**
|
|
232
315
|
* Returns true when `namespace` is a non-leaf key in `contributions.field`.
|
|
233
316
|
*
|
|
@@ -341,10 +424,127 @@ function collectAuthoringLeafPaths(
|
|
|
341
424
|
return paths;
|
|
342
425
|
}
|
|
343
426
|
|
|
427
|
+
interface AuthoringLeafEntry {
|
|
428
|
+
readonly path: string;
|
|
429
|
+
readonly discriminator: string;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function collectAuthoringLeafDiscriminators(
|
|
433
|
+
namespace: Readonly<Record<string, unknown>>,
|
|
434
|
+
isLeaf: (value: unknown) => boolean,
|
|
435
|
+
label: string,
|
|
436
|
+
path: readonly string[] = [],
|
|
437
|
+
): AuthoringLeafEntry[] {
|
|
438
|
+
const entries: AuthoringLeafEntry[] = [];
|
|
439
|
+
for (const [key, value] of Object.entries(namespace)) {
|
|
440
|
+
const currentPath = [...path, key];
|
|
441
|
+
if (isLeaf(value)) {
|
|
442
|
+
const record = blindCast<
|
|
443
|
+
Record<string, unknown>,
|
|
444
|
+
'discriminator extraction from a leaf already validated by isLeaf'
|
|
445
|
+
>(value);
|
|
446
|
+
const discriminator = record['discriminator'];
|
|
447
|
+
if (typeof discriminator === 'string' && discriminator.length > 0) {
|
|
448
|
+
entries.push({ path: currentPath.join('.'), discriminator });
|
|
449
|
+
}
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
453
|
+
const record = blindCast<
|
|
454
|
+
Readonly<Record<string, unknown>>,
|
|
455
|
+
'walker inspects a non-leaf value for descriptor-shaped keys before recursing'
|
|
456
|
+
>(value);
|
|
457
|
+
// A value carrying descriptor-shaped keys (`kind`/`keyword`/`discriminator`)
|
|
458
|
+
// but failing `isAuthoringPslBlockDescriptor` (e.g. missing `parameters`) is
|
|
459
|
+
// a malformed declarative descriptor. Descending into it as a sub-namespace
|
|
460
|
+
// would silently skip it, so a half-built contribution would pass validation.
|
|
461
|
+
// Reject it at load time instead, naming the path and what's wrong.
|
|
462
|
+
//
|
|
463
|
+
// A valid sub-namespace whose key happens to be named `kind`, `keyword`, or
|
|
464
|
+
// `discriminator` (but which does not look like a descriptor overall) must
|
|
465
|
+
// still descend normally — the check requires descriptor-shaped keys present
|
|
466
|
+
// AND the leaf guard rejecting it.
|
|
467
|
+
if (
|
|
468
|
+
(record['kind'] !== undefined ||
|
|
469
|
+
record['keyword'] !== undefined ||
|
|
470
|
+
record['discriminator'] !== undefined) &&
|
|
471
|
+
!isLeaf(value)
|
|
472
|
+
) {
|
|
473
|
+
const hasKind = record['kind'] === 'pslBlock';
|
|
474
|
+
const hasKeyword = typeof record['keyword'] === 'string';
|
|
475
|
+
const hasDiscriminator = typeof record['discriminator'] === 'string';
|
|
476
|
+
if (hasKind || (hasKeyword && hasDiscriminator)) {
|
|
477
|
+
throw new Error(
|
|
478
|
+
`Malformed authoring ${label} contribution at "${currentPath.join('.')}". The value carries descriptor keys (kind/keyword/discriminator) but does not satisfy the ${label} descriptor shape. Fix the contribution so it is a complete descriptor, or remove the stray keys if it was meant to be a sub-namespace.`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
entries.push(...collectAuthoringLeafDiscriminators(record, isLeaf, label, currentPath));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return entries;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Throws when two or more entries in the same namespace share a discriminator.
|
|
490
|
+
* Duplicate discriminators within a namespace make dispatch ambiguous — the
|
|
491
|
+
* lowering factory lookup dispatches by discriminator, so one would silently
|
|
492
|
+
* shadow the other. Catch duplicates before building any dispatch map.
|
|
493
|
+
*/
|
|
494
|
+
function assertUniqueDiscriminators(entries: readonly AuthoringLeafEntry[], label: string): void {
|
|
495
|
+
const seen = new Map<string, string>();
|
|
496
|
+
for (const { path, discriminator } of entries) {
|
|
497
|
+
const existing = seen.get(discriminator);
|
|
498
|
+
if (existing !== undefined) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
`Duplicate ${label} discriminator "${discriminator}" registered at both "${existing}" and "${path}". Each ${label} contribution must use a unique discriminator.`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
seen.set(discriminator, path);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Every `pslBlockDescriptors` entry needs a matching `entityTypes` factory
|
|
509
|
+
* (same discriminator): the parser would otherwise produce an AST node
|
|
510
|
+
* nothing can lower to an IR class instance. The link is one-directional
|
|
511
|
+
* — an `entityTypes` factory may stand alone (e.g. `enum`, reachable from
|
|
512
|
+
* the TypeScript builder without any PSL block).
|
|
513
|
+
*/
|
|
514
|
+
function assertPslBlocksHaveFactories(
|
|
515
|
+
entityTypeNamespace: AuthoringEntityTypeNamespace,
|
|
516
|
+
pslBlockNamespace: AuthoringPslBlockDescriptorNamespace,
|
|
517
|
+
): void {
|
|
518
|
+
const blockEntries = collectAuthoringLeafDiscriminators(
|
|
519
|
+
pslBlockNamespace,
|
|
520
|
+
isAuthoringPslBlockDescriptor,
|
|
521
|
+
'pslBlock',
|
|
522
|
+
);
|
|
523
|
+
const entityEntries = collectAuthoringLeafDiscriminators(
|
|
524
|
+
entityTypeNamespace,
|
|
525
|
+
isAuthoringEntityTypeDescriptor,
|
|
526
|
+
'entityType',
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
assertUniqueDiscriminators(blockEntries, 'pslBlock');
|
|
530
|
+
assertUniqueDiscriminators(entityEntries, 'entityType');
|
|
531
|
+
|
|
532
|
+
const entityDiscriminators = new Set(entityEntries.map((entry) => entry.discriminator));
|
|
533
|
+
|
|
534
|
+
for (const block of blockEntries) {
|
|
535
|
+
if (!entityDiscriminators.has(block.discriminator)) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`Incomplete extension contribution: pslBlock helper "${block.path}" registers discriminator "${block.discriminator}" but no entityType contribution shares that discriminator. An extension-contributed PSL block requires a matching entityType factory so the parsed AST node can lower to an IR class instance; add an entityType helper with discriminator "${block.discriminator}".`,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
344
543
|
export function assertNoCrossRegistryCollisions(
|
|
345
544
|
typeNamespace: AuthoringTypeNamespace,
|
|
346
545
|
fieldNamespace: AuthoringFieldNamespace,
|
|
347
546
|
entityTypeNamespace: AuthoringEntityTypeNamespace = {},
|
|
547
|
+
pslBlockNamespace: AuthoringPslBlockDescriptorNamespace = {},
|
|
348
548
|
): void {
|
|
349
549
|
const typePaths = new Set(
|
|
350
550
|
collectAuthoringLeafPaths(typeNamespace, isAuthoringTypeConstructorDescriptor),
|
|
@@ -360,20 +560,33 @@ export function assertNoCrossRegistryCollisions(
|
|
|
360
560
|
// `mergeHelperNamespaces` in composed-authoring-helpers.ts), which throws
|
|
361
561
|
// on same-path registrations within any single registry before this check
|
|
362
562
|
// runs. This function only handles the cross-registry case.
|
|
563
|
+
//
|
|
564
|
+
// Cross-registry collisions are checked among `type` / `field` /
|
|
565
|
+
// `entityTypes` only — these three are user-facing helper paths that PSL
|
|
566
|
+
// must resolve unambiguously. `pslBlockDescriptors` is an internal
|
|
567
|
+
// framework index consumed by parser and printer dispatch, not a
|
|
568
|
+
// user-facing helper path; the natural authoring pattern is the same
|
|
569
|
+
// path key in `entityTypes` and `pslBlockDescriptors` for a single
|
|
570
|
+
// contribution. The block→factory link is enforced by
|
|
571
|
+
// `assertPslBlocksHaveFactories` via the discriminator string, not by path.
|
|
572
|
+
const ambiguityHint =
|
|
573
|
+
'Register each path in only one of authoringContributions.field / authoringContributions.type / authoringContributions.entityTypes.';
|
|
363
574
|
for (const fieldPath of fieldPaths) {
|
|
364
575
|
if (typePaths.has(fieldPath)) {
|
|
365
576
|
throw new Error(
|
|
366
|
-
`Ambiguous authoring registry path "${fieldPath}". The same path is registered as both a type constructor and a field preset; PSL resolution would be ambiguous.
|
|
577
|
+
`Ambiguous authoring registry path "${fieldPath}". The same path is registered as both a type constructor and a field preset; PSL resolution would be ambiguous. ${ambiguityHint}`,
|
|
367
578
|
);
|
|
368
579
|
}
|
|
369
580
|
}
|
|
370
581
|
for (const entityPath of entityPaths) {
|
|
371
582
|
if (typePaths.has(entityPath) || fieldPaths.has(entityPath)) {
|
|
372
583
|
throw new Error(
|
|
373
|
-
`Ambiguous authoring registry path "${entityPath}". The same path is registered as an entity contribution AND as a type constructor or field preset; PSL resolution would be ambiguous.
|
|
584
|
+
`Ambiguous authoring registry path "${entityPath}". The same path is registered as an entity contribution AND as a type constructor or field preset; PSL resolution would be ambiguous. ${ambiguityHint}`,
|
|
374
585
|
);
|
|
375
586
|
}
|
|
376
587
|
}
|
|
588
|
+
|
|
589
|
+
assertPslBlocksHaveFactories(entityTypeNamespace, pslBlockNamespace);
|
|
377
590
|
}
|
|
378
591
|
|
|
379
592
|
export function resolveAuthoringTemplateValue(
|
|
@@ -227,6 +227,8 @@ export type TargetPackRef<
|
|
|
227
227
|
TTargetId extends string = string,
|
|
228
228
|
> = PackRefBase<'target', TFamilyId> & {
|
|
229
229
|
readonly targetId: TTargetId;
|
|
230
|
+
/** The namespace a bare (un-namespaced) entity name resolves to for this target (e.g. Postgres `'public'`). */
|
|
231
|
+
readonly defaultNamespaceId: string;
|
|
230
232
|
};
|
|
231
233
|
|
|
232
234
|
export type AdapterPackRef<
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape-only types for the PSL source-position primitives, diagnostic
|
|
3
|
+
* codes, extension-block descriptor vocabulary, and the uniform
|
|
4
|
+
* extension-block AST node base.
|
|
5
|
+
*
|
|
6
|
+
* These live in the shared plane so an extension's authoring descriptor
|
|
7
|
+
* (`AuthoringPslBlockDescriptor` in `framework-authoring`) can reference
|
|
8
|
+
* them without crossing the shared → migration-plane boundary. The
|
|
9
|
+
* migration-plane `psl-ast.ts` re-exports everything here for consumers
|
|
10
|
+
* that import PSL AST types from the control entrypoint.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface PslPosition {
|
|
14
|
+
readonly offset: number;
|
|
15
|
+
readonly line: number;
|
|
16
|
+
readonly column: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PslSpan {
|
|
20
|
+
readonly start: PslPosition;
|
|
21
|
+
readonly end: PslPosition;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type PslDiagnosticCode =
|
|
25
|
+
| 'PSL_UNTERMINATED_BLOCK'
|
|
26
|
+
| 'PSL_UNSUPPORTED_TOP_LEVEL_BLOCK'
|
|
27
|
+
| 'PSL_INVALID_NAMESPACE_BLOCK'
|
|
28
|
+
| 'PSL_INVALID_ATTRIBUTE_SYNTAX'
|
|
29
|
+
| 'PSL_INVALID_MODEL_MEMBER'
|
|
30
|
+
| 'PSL_UNSUPPORTED_MODEL_ATTRIBUTE'
|
|
31
|
+
| 'PSL_UNSUPPORTED_FIELD_ATTRIBUTE'
|
|
32
|
+
| 'PSL_INVALID_RELATION_ATTRIBUTE'
|
|
33
|
+
| 'PSL_INVALID_REFERENTIAL_ACTION'
|
|
34
|
+
| 'PSL_INVALID_DEFAULT_VALUE'
|
|
35
|
+
| 'PSL_INVALID_ENUM_MEMBER'
|
|
36
|
+
| 'PSL_INVALID_TYPES_MEMBER'
|
|
37
|
+
| 'PSL_INVALID_QUALIFIED_TYPE'
|
|
38
|
+
/**
|
|
39
|
+
* A malformed line inside an extension-contributed top-level block body, or
|
|
40
|
+
* a structurally invalid element inside a `list` parameter value.
|
|
41
|
+
*
|
|
42
|
+
* Replaces the overloaded `PSL_UNSUPPORTED_TOP_LEVEL_BLOCK` code that the
|
|
43
|
+
* generic framework parser previously used for these two parse-error sites
|
|
44
|
+
* inside extension blocks — keeping `PSL_UNSUPPORTED_TOP_LEVEL_BLOCK` for
|
|
45
|
+
* its original meaning (an unknown keyword at the top level) and giving
|
|
46
|
+
* extension-block parse errors their own code.
|
|
47
|
+
*/
|
|
48
|
+
| 'PSL_INVALID_EXTENSION_BLOCK_MEMBER'
|
|
49
|
+
/**
|
|
50
|
+
* An unknown parameter key in an extension-contributed block — a key present
|
|
51
|
+
* in the source block but absent from the descriptor's `parameters` map.
|
|
52
|
+
*/
|
|
53
|
+
| 'PSL_EXTENSION_UNKNOWN_PARAMETER'
|
|
54
|
+
/**
|
|
55
|
+
* A required parameter declared in the descriptor is absent from the parsed block.
|
|
56
|
+
*/
|
|
57
|
+
| 'PSL_EXTENSION_MISSING_REQUIRED_PARAMETER'
|
|
58
|
+
/**
|
|
59
|
+
* An `option`-kind parameter value is not one of the allowed tokens listed
|
|
60
|
+
* in the descriptor's `values` array.
|
|
61
|
+
*/
|
|
62
|
+
| 'PSL_EXTENSION_OPTION_OUT_OF_SET'
|
|
63
|
+
/**
|
|
64
|
+
* A `value`-kind parameter's raw text is not a valid JSON literal, or the
|
|
65
|
+
* parsed JSON value was rejected by the codec's `decodeJson` method, or the
|
|
66
|
+
* codec id is not registered in the lookup.
|
|
67
|
+
*/
|
|
68
|
+
| 'PSL_EXTENSION_INVALID_VALUE'
|
|
69
|
+
/**
|
|
70
|
+
* A `ref`-kind parameter identifier does not resolve to a declared entity of
|
|
71
|
+
* the required `refKind` within the declared scope.
|
|
72
|
+
*/
|
|
73
|
+
| 'PSL_EXTENSION_UNRESOLVED_REF';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Descriptor vocabulary for a single parameter on a declared block.
|
|
77
|
+
*
|
|
78
|
+
* Four kinds:
|
|
79
|
+
* - `ref` — the parameter value is an identifier that must resolve to a
|
|
80
|
+
* declared entity of `refKind` within the declared `scope`.
|
|
81
|
+
* - `value` — the parameter value is a PSL literal parsed and printed
|
|
82
|
+
* through the codec identified by `codecId`.
|
|
83
|
+
* - `option` — the parameter value is one of the literal tokens in `values`.
|
|
84
|
+
* Not a codec; not persisted data. A closed authoring-time constraint only.
|
|
85
|
+
* - `list` — a bracketed list whose elements each match the `of` descriptor.
|
|
86
|
+
*/
|
|
87
|
+
export type PslBlockParam =
|
|
88
|
+
| PslBlockParamRef
|
|
89
|
+
| PslBlockParamValue
|
|
90
|
+
| PslBlockParamOption
|
|
91
|
+
| PslBlockParamList;
|
|
92
|
+
|
|
93
|
+
export interface PslBlockParamRef {
|
|
94
|
+
readonly kind: 'ref';
|
|
95
|
+
readonly refKind: string;
|
|
96
|
+
readonly scope: 'same-namespace' | 'same-space' | 'cross-space';
|
|
97
|
+
readonly required?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface PslBlockParamValue {
|
|
101
|
+
readonly kind: 'value';
|
|
102
|
+
readonly codecId: string;
|
|
103
|
+
readonly required?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface PslBlockParamOption {
|
|
107
|
+
readonly kind: 'option';
|
|
108
|
+
readonly values: readonly string[];
|
|
109
|
+
readonly required?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface PslBlockParamList {
|
|
113
|
+
readonly kind: 'list';
|
|
114
|
+
readonly of: PslBlockParam;
|
|
115
|
+
readonly required?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The parsed representation of a single parameter value on a uniform
|
|
120
|
+
* extension-block AST node. Mirrors the `PslBlockParam` descriptor
|
|
121
|
+
* vocabulary:
|
|
122
|
+
*
|
|
123
|
+
* - `ref` → `PslExtensionBlockParamRef` — a raw identifier string
|
|
124
|
+
* (resolution runs in the validator, not the parser).
|
|
125
|
+
* - `value` → `PslExtensionBlockParamValue` — a raw PSL literal string
|
|
126
|
+
* (codec validation runs in the validator).
|
|
127
|
+
* - `option` → `PslExtensionBlockParamOption` — the chosen token.
|
|
128
|
+
* - `list` → `PslExtensionBlockParamList` — ordered list of the above.
|
|
129
|
+
*
|
|
130
|
+
* These shapes are intentionally minimal. The validator and lowering refine
|
|
131
|
+
* and consume them; the generic framework parser produces them.
|
|
132
|
+
*/
|
|
133
|
+
export type PslExtensionBlockParamValue =
|
|
134
|
+
| PslExtensionBlockParamRef
|
|
135
|
+
| PslExtensionBlockParamScalarValue
|
|
136
|
+
| PslExtensionBlockParamOption
|
|
137
|
+
| PslExtensionBlockParamList;
|
|
138
|
+
|
|
139
|
+
export interface PslExtensionBlockParamRef {
|
|
140
|
+
readonly kind: 'ref';
|
|
141
|
+
readonly identifier: string;
|
|
142
|
+
readonly span: PslSpan;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface PslExtensionBlockParamScalarValue {
|
|
146
|
+
readonly kind: 'value';
|
|
147
|
+
readonly raw: string;
|
|
148
|
+
readonly span: PslSpan;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface PslExtensionBlockParamOption {
|
|
152
|
+
readonly kind: 'option';
|
|
153
|
+
readonly token: string;
|
|
154
|
+
readonly span: PslSpan;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface PslExtensionBlockParamList {
|
|
158
|
+
readonly kind: 'list';
|
|
159
|
+
readonly items: readonly PslExtensionBlockParamValue[];
|
|
160
|
+
readonly span: PslSpan;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Base shape for a uniform extension-contributed top-level PSL block
|
|
165
|
+
* node, as produced by the generic framework parser and consumed by the
|
|
166
|
+
* validator and lowering factory.
|
|
167
|
+
*
|
|
168
|
+
* - `kind` is the routing discriminant, equal to the descriptor's
|
|
169
|
+
* `discriminator`. The framework parser sets this to
|
|
170
|
+
* `descriptor.discriminator` for every block it parses.
|
|
171
|
+
* - `name` is the block's declared name (the identifier after the keyword).
|
|
172
|
+
* - `parameters` is the descriptor-driven parameter map. Keys are
|
|
173
|
+
* parameter names from the descriptor; values are the parsed parameter
|
|
174
|
+
* representations. Only parameters present in the source are included
|
|
175
|
+
* — absence of a required parameter is a validator concern, not a
|
|
176
|
+
* parser concern.
|
|
177
|
+
* - `span` covers the full block from keyword to closing brace.
|
|
178
|
+
*/
|
|
179
|
+
export interface PslExtensionBlock {
|
|
180
|
+
readonly kind: string;
|
|
181
|
+
readonly name: string;
|
|
182
|
+
readonly parameters: Record<string, PslExtensionBlockParamValue>;
|
|
183
|
+
readonly span: PslSpan;
|
|
184
|
+
}
|