@prisma-next/framework-components 0.12.0 → 0.13.0-dev.1
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-R0TYCkvG.d.mts +380 -0
- package/dist/framework-authoring-R0TYCkvG.d.mts.map +1 -0
- package/dist/{framework-components-CuoUhyB5.d.mts → framework-components-DDQXmW0b.d.mts} +6 -5
- package/dist/{framework-components-CuoUhyB5.d.mts.map → framework-components-DDQXmW0b.d.mts.map} +1 -1
- 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/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-Cn50B-UG.d.mts} +90 -18
- package/dist/psl-ast-Cn50B-UG.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 +222 -4
- 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 +234 -34
- package/src/control/psl-extension-block-validator.ts +324 -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
|
@@ -13,7 +13,11 @@ import type { Contract } from '@prisma-next/contract/types';
|
|
|
13
13
|
import type { ImportRequirement } from '@prisma-next/ts-render';
|
|
14
14
|
import type { Result } from '@prisma-next/utils/result';
|
|
15
15
|
import type { TargetBoundComponentDescriptor } from '../shared/framework-components';
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
ControlAdapterInstance,
|
|
18
|
+
ControlDriverInstance,
|
|
19
|
+
ControlFamilyInstance,
|
|
20
|
+
} from './control-instances';
|
|
17
21
|
import type { OperationContext } from './control-result-types';
|
|
18
22
|
|
|
19
23
|
// ============================================================================
|
|
@@ -279,6 +283,7 @@ export interface MigrationPlannerConflict {
|
|
|
279
283
|
export interface MigrationPlannerSuccessResult {
|
|
280
284
|
readonly kind: 'success';
|
|
281
285
|
readonly plan: MigrationPlanWithAuthoringSurface;
|
|
286
|
+
readonly warnings?: readonly MigrationPlannerConflict[];
|
|
282
287
|
}
|
|
283
288
|
|
|
284
289
|
/**
|
|
@@ -479,6 +484,17 @@ export interface MigrationRunnerPerSpaceOptions<
|
|
|
479
484
|
* Paths and metadata forwarded to schema verification diagnostics.
|
|
480
485
|
*/
|
|
481
486
|
readonly context?: OperationContext;
|
|
487
|
+
/**
|
|
488
|
+
* Per-edge breakdown from aggregate planning. Runners write one ledger row
|
|
489
|
+
* per edge in walk order.
|
|
490
|
+
*/
|
|
491
|
+
readonly migrationEdges: ReadonlyArray<{
|
|
492
|
+
readonly migrationHash: string;
|
|
493
|
+
readonly dirName: string;
|
|
494
|
+
readonly from: string;
|
|
495
|
+
readonly to: string;
|
|
496
|
+
readonly operationCount: number;
|
|
497
|
+
}>;
|
|
482
498
|
}
|
|
483
499
|
|
|
484
500
|
export interface MigrationRunner<
|
|
@@ -525,7 +541,9 @@ export interface TargetMigrationsCapability<
|
|
|
525
541
|
unknown
|
|
526
542
|
>,
|
|
527
543
|
> {
|
|
528
|
-
createPlanner(
|
|
544
|
+
createPlanner(
|
|
545
|
+
adapter: ControlAdapterInstance<TFamilyId, TTargetId>,
|
|
546
|
+
): MigrationPlanner<TFamilyId, TTargetId>;
|
|
529
547
|
createRunner(family: TFamilyInstance): MigrationRunner<TFamilyId, TTargetId>;
|
|
530
548
|
/**
|
|
531
549
|
* Synthesizes a family-specific schema IR from a contract for offline planning.
|
|
@@ -58,7 +58,10 @@ export interface BaseSchemaIssue {
|
|
|
58
58
|
| 'index_mismatch'
|
|
59
59
|
| 'default_missing'
|
|
60
60
|
| 'default_mismatch'
|
|
61
|
-
| 'extra_default'
|
|
61
|
+
| 'extra_default'
|
|
62
|
+
| 'check_missing'
|
|
63
|
+
| 'check_removed'
|
|
64
|
+
| 'check_mismatch';
|
|
62
65
|
readonly table?: string;
|
|
63
66
|
/**
|
|
64
67
|
* Namespace coordinate of the issue's subject (e.g. the schema a SQL
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
1
2
|
import type { Codec } from '../shared/codec';
|
|
2
3
|
import type { CodecLookup, CodecMeta } from '../shared/codec-types';
|
|
3
4
|
import type {
|
|
4
5
|
AuthoringContributions,
|
|
5
6
|
AuthoringEntityTypeNamespace,
|
|
6
7
|
AuthoringFieldNamespace,
|
|
8
|
+
AuthoringPslBlockDescriptorNamespace,
|
|
7
9
|
AuthoringTypeNamespace,
|
|
8
10
|
} from '../shared/framework-authoring';
|
|
9
11
|
import {
|
|
10
12
|
assertNoCrossRegistryCollisions,
|
|
11
13
|
isAuthoringEntityTypeDescriptor,
|
|
12
14
|
isAuthoringFieldPresetDescriptor,
|
|
15
|
+
isAuthoringPslBlockDescriptor,
|
|
13
16
|
isAuthoringTypeConstructorDescriptor,
|
|
14
17
|
mergeAuthoringNamespaces,
|
|
15
18
|
} from '../shared/framework-authoring';
|
|
@@ -32,6 +35,7 @@ export interface AssembledAuthoringContributions {
|
|
|
32
35
|
readonly field: AuthoringFieldNamespace;
|
|
33
36
|
readonly type: AuthoringTypeNamespace;
|
|
34
37
|
readonly entityTypes: AuthoringEntityTypeNamespace;
|
|
38
|
+
readonly pslBlockDescriptors: AuthoringPslBlockDescriptorNamespace;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export interface ControlStack<
|
|
@@ -151,6 +155,7 @@ export function assembleAuthoringContributions(
|
|
|
151
155
|
const field = {} as Record<string, unknown>;
|
|
152
156
|
const type = {} as Record<string, unknown>;
|
|
153
157
|
const entityTypes = {} as Record<string, unknown>;
|
|
158
|
+
const pslBlockDescriptors: Record<string, unknown> = {};
|
|
154
159
|
|
|
155
160
|
for (const descriptor of descriptors) {
|
|
156
161
|
if (descriptor.authoring?.field) {
|
|
@@ -180,17 +185,36 @@ export function assembleAuthoringContributions(
|
|
|
180
185
|
'entity',
|
|
181
186
|
);
|
|
182
187
|
}
|
|
188
|
+
if (descriptor.authoring?.pslBlockDescriptors) {
|
|
189
|
+
mergeAuthoringNamespaces(
|
|
190
|
+
pslBlockDescriptors,
|
|
191
|
+
descriptor.authoring.pslBlockDescriptors,
|
|
192
|
+
[],
|
|
193
|
+
isAuthoringPslBlockDescriptor,
|
|
194
|
+
'pslBlock',
|
|
195
|
+
);
|
|
196
|
+
}
|
|
183
197
|
}
|
|
184
198
|
|
|
185
199
|
const fieldNamespace = field as AuthoringFieldNamespace;
|
|
186
200
|
const typeNamespace = type as AuthoringTypeNamespace;
|
|
187
201
|
const entityTypeNamespace = entityTypes as AuthoringEntityTypeNamespace;
|
|
188
|
-
|
|
202
|
+
const pslBlockDescriptorNamespace = blindCast<
|
|
203
|
+
AuthoringPslBlockDescriptorNamespace,
|
|
204
|
+
'merge target accumulator narrows to typed namespace post-merge'
|
|
205
|
+
>(pslBlockDescriptors);
|
|
206
|
+
assertNoCrossRegistryCollisions(
|
|
207
|
+
typeNamespace,
|
|
208
|
+
fieldNamespace,
|
|
209
|
+
entityTypeNamespace,
|
|
210
|
+
pslBlockDescriptorNamespace,
|
|
211
|
+
);
|
|
189
212
|
|
|
190
213
|
return {
|
|
191
214
|
field: fieldNamespace,
|
|
192
215
|
type: typeNamespace,
|
|
193
216
|
entityTypes: entityTypeNamespace,
|
|
217
|
+
pslBlockDescriptors: pslBlockDescriptorNamespace,
|
|
194
218
|
};
|
|
195
219
|
}
|
|
196
220
|
|
|
@@ -345,12 +369,107 @@ export function validateScalarTypeCodecIds(
|
|
|
345
369
|
return errors;
|
|
346
370
|
}
|
|
347
371
|
|
|
372
|
+
interface DependencyDeclaringDescriptor {
|
|
373
|
+
readonly id: string;
|
|
374
|
+
readonly contractSpace?: {
|
|
375
|
+
readonly contractJson?: {
|
|
376
|
+
readonly extensionPacks?: Readonly<Record<string, unknown>>;
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function readDeclaredDependencyIds(descriptor: DependencyDeclaringDescriptor): readonly string[] {
|
|
382
|
+
const packs = descriptor.contractSpace?.contractJson?.extensionPacks;
|
|
383
|
+
if (packs === null || typeof packs !== 'object') return [];
|
|
384
|
+
return Object.keys(packs);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Builds a dependency-respecting load order for the given extension descriptors
|
|
389
|
+
* using Kahn's topological sort algorithm. Dependencies (packs declared in
|
|
390
|
+
* `contractSpace.contractJson.extensionPacks`) are placed before the extensions
|
|
391
|
+
* that depend on them.
|
|
392
|
+
*
|
|
393
|
+
* Throws if the dependency graph contains a cycle, with an error message that
|
|
394
|
+
* names every extension involved in the cycle.
|
|
395
|
+
*
|
|
396
|
+
* Throws if any extension declares a dependency on a pack ID that is not present
|
|
397
|
+
* in the provided list — add the missing pack to the `extensionPacks` list to
|
|
398
|
+
* resolve the error.
|
|
399
|
+
*/
|
|
400
|
+
|
|
401
|
+
export function buildExtensionLoadOrder(
|
|
402
|
+
extensions: ReadonlyArray<DependencyDeclaringDescriptor>,
|
|
403
|
+
): readonly string[] {
|
|
404
|
+
if (extensions.length === 0) return [];
|
|
405
|
+
|
|
406
|
+
const idSet = new Set(extensions.map((e) => e.id));
|
|
407
|
+
const inDegree = new Map<string, number>();
|
|
408
|
+
const dependents = new Map<string, string[]>();
|
|
409
|
+
|
|
410
|
+
for (const ext of extensions) {
|
|
411
|
+
if (!inDegree.has(ext.id)) inDegree.set(ext.id, 0);
|
|
412
|
+
if (!dependents.has(ext.id)) dependents.set(ext.id, []);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
for (const ext of extensions) {
|
|
416
|
+
for (const depId of readDeclaredDependencyIds(ext)) {
|
|
417
|
+
if (!idSet.has(depId)) {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`Extension "${ext.id}" declares a dependency on "${depId}", but "${depId}" is not in the provided extension set. Add the missing space to extensionPacks.`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
inDegree.set(ext.id, (inDegree.get(ext.id) ?? 0) + 1);
|
|
423
|
+
const list = dependents.get(depId);
|
|
424
|
+
if (list !== undefined) list.push(ext.id);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const queue: string[] = [];
|
|
429
|
+
for (const [id, deg] of inDegree) {
|
|
430
|
+
if (deg === 0) queue.push(id);
|
|
431
|
+
}
|
|
432
|
+
queue.sort();
|
|
433
|
+
|
|
434
|
+
const result: string[] = [];
|
|
435
|
+
while (queue.length > 0) {
|
|
436
|
+
const id = queue.shift();
|
|
437
|
+
if (id === undefined) break;
|
|
438
|
+
result.push(id);
|
|
439
|
+
const children = dependents.get(id) ?? [];
|
|
440
|
+
children.sort();
|
|
441
|
+
for (const childId of children) {
|
|
442
|
+
const newDeg = (inDegree.get(childId) ?? 1) - 1;
|
|
443
|
+
inDegree.set(childId, newDeg);
|
|
444
|
+
if (newDeg === 0) queue.push(childId);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (result.length < extensions.length) {
|
|
449
|
+
const cycleMembers = extensions
|
|
450
|
+
.map((e) => e.id)
|
|
451
|
+
.filter((id) => !result.includes(id))
|
|
452
|
+
.sort();
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Extension dependency cycle detected. Cycle members: ${cycleMembers.map((id) => `"${id}"`).join(', ')}.`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
|
|
348
461
|
export function createControlStack<TFamilyId extends string, TTargetId extends string>(
|
|
349
462
|
input: CreateControlStackInput<TFamilyId, TTargetId>,
|
|
350
463
|
): ControlStack<TFamilyId, TTargetId> {
|
|
351
464
|
const { family, target, adapter, driver, extensionPacks = [] } = input;
|
|
352
465
|
|
|
353
|
-
const
|
|
466
|
+
const orderedIds = buildExtensionLoadOrder(extensionPacks);
|
|
467
|
+
const extensionById = new Map(extensionPacks.map((ext) => [ext.id, ext]));
|
|
468
|
+
const orderedExtensionPacks = orderedIds
|
|
469
|
+
.map((id) => extensionById.get(id))
|
|
470
|
+
.filter((ext): ext is ControlExtensionDescriptor<TFamilyId, TTargetId> => ext !== undefined);
|
|
471
|
+
|
|
472
|
+
const allDescriptors = [family, target, ...(adapter ? [adapter] : []), ...orderedExtensionPacks];
|
|
354
473
|
|
|
355
474
|
const codecLookup = extractCodecLookup(allDescriptors);
|
|
356
475
|
const scalarTypeDescriptors = assembleScalarTypeDescriptors(allDescriptors);
|
|
@@ -360,11 +479,11 @@ export function createControlStack<TFamilyId extends string, TTargetId extends s
|
|
|
360
479
|
target,
|
|
361
480
|
adapter,
|
|
362
481
|
driver,
|
|
363
|
-
extensionPacks:
|
|
482
|
+
extensionPacks: orderedExtensionPacks,
|
|
364
483
|
|
|
365
484
|
codecTypeImports: extractCodecTypeImports(allDescriptors),
|
|
366
485
|
queryOperationTypeImports: extractQueryOperationTypeImports(allDescriptors),
|
|
367
|
-
extensionIds: extractComponentIds(family, target, adapter,
|
|
486
|
+
extensionIds: extractComponentIds(family, target, adapter, orderedExtensionPacks),
|
|
368
487
|
codecLookup,
|
|
369
488
|
authoringContributions: assembleAuthoringContributions(allDescriptors),
|
|
370
489
|
scalarTypeDescriptors,
|
package/src/control/psl-ast.ts
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
| 'PSL_INVALID_ENUM_MEMBER'
|
|
24
|
-
| 'PSL_INVALID_TYPES_MEMBER'
|
|
25
|
-
| 'PSL_INVALID_QUALIFIED_TYPE';
|
|
1
|
+
export type { AuthoringPslBlockDescriptorNamespace } from '../shared/framework-authoring';
|
|
2
|
+
export type {
|
|
3
|
+
PslBlockParam,
|
|
4
|
+
PslBlockParamList,
|
|
5
|
+
PslBlockParamOption,
|
|
6
|
+
PslBlockParamRef,
|
|
7
|
+
PslBlockParamValue,
|
|
8
|
+
PslDiagnosticCode,
|
|
9
|
+
PslExtensionBlock,
|
|
10
|
+
PslExtensionBlockParamList,
|
|
11
|
+
PslExtensionBlockParamOption,
|
|
12
|
+
PslExtensionBlockParamRef,
|
|
13
|
+
PslExtensionBlockParamScalarValue,
|
|
14
|
+
PslExtensionBlockParamValue,
|
|
15
|
+
PslPosition,
|
|
16
|
+
PslSpan,
|
|
17
|
+
} from '../shared/psl-extension-block';
|
|
18
|
+
|
|
19
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
20
|
+
import type { CodecLookup } from '../shared/codec-types';
|
|
21
|
+
import type { AuthoringPslBlockDescriptorNamespace } from '../shared/framework-authoring';
|
|
22
|
+
import type { PslDiagnosticCode, PslExtensionBlock, PslSpan } from '../shared/psl-extension-block';
|
|
26
23
|
|
|
27
24
|
export interface PslDiagnostic {
|
|
28
25
|
readonly code: PslDiagnosticCode;
|
|
@@ -82,10 +79,19 @@ export type PslFieldAttribute = PslAttribute;
|
|
|
82
79
|
export interface PslField {
|
|
83
80
|
readonly kind: 'field';
|
|
84
81
|
readonly name: string;
|
|
85
|
-
/** Unqualified type name, e.g. `"User"` for both `User` and `auth.User`. */
|
|
82
|
+
/** Unqualified type name, e.g. `"User"` for both `User`, `auth.User`, and `supabase:auth.User`. */
|
|
86
83
|
readonly typeName: string;
|
|
87
|
-
/** Namespace qualifier from a dot-qualified type reference, e.g. `"auth"` for `auth.User`. Absent for unqualified types. */
|
|
84
|
+
/** Namespace qualifier from a dot-qualified type reference, e.g. `"auth"` for `auth.User` or `supabase:auth.User`. Absent for unqualified types. */
|
|
88
85
|
readonly typeNamespaceId?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Contract-space qualifier from a colon-prefix type reference, e.g. `"supabase"` for
|
|
88
|
+
* `supabase:auth.User` or `supabase:User`. Absent for local (same-space) type references.
|
|
89
|
+
*
|
|
90
|
+
* When present, the field references a model from a different contract space. The namespace
|
|
91
|
+
* (`typeNamespaceId`) and model name (`typeName`) identify the target within that space.
|
|
92
|
+
* Physical table resolution against the extension contract is deferred to the aggregate stage (M3).
|
|
93
|
+
*/
|
|
94
|
+
readonly typeContractSpaceId?: string;
|
|
89
95
|
readonly typeConstructor?: PslTypeConstructorCall;
|
|
90
96
|
readonly optional: boolean;
|
|
91
97
|
readonly list: boolean;
|
|
@@ -148,6 +154,11 @@ export interface PslEnum {
|
|
|
148
154
|
readonly span: PslSpan;
|
|
149
155
|
}
|
|
150
156
|
|
|
157
|
+
/**
|
|
158
|
+
* A reusable group of fields embedded in a model (a `type Name { … }` block) —
|
|
159
|
+
* e.g. a MongoDB embedded document or a Postgres composite type. Unlike
|
|
160
|
+
* {@link PslModel} it has no storage or identity of its own.
|
|
161
|
+
*/
|
|
151
162
|
export interface PslCompositeType {
|
|
152
163
|
readonly kind: 'compositeType';
|
|
153
164
|
readonly name: string;
|
|
@@ -190,22 +201,135 @@ export interface PslTypesBlock {
|
|
|
190
201
|
*/
|
|
191
202
|
export const UNSPECIFIED_PSL_NAMESPACE_ID = '__unspecified__';
|
|
192
203
|
|
|
204
|
+
/** A value in {@link PslNamespace.entries}: a built-in entity node or an extension-contributed {@link PslExtensionBlock}. */
|
|
205
|
+
export type PslNamespaceEntry = PslModel | PslEnum | PslCompositeType | PslExtensionBlock;
|
|
206
|
+
|
|
193
207
|
/**
|
|
194
|
-
* A
|
|
195
|
-
* `
|
|
196
|
-
* `namespace { … }` block. Multiple `namespace foo { … }` blocks for the
|
|
197
|
-
* same name across one or more files reopen-merge into a single entry;
|
|
208
|
+
* A namespace block, or the parser's synthesised `__unspecified__` bucket for
|
|
209
|
+
* declarations outside any `namespace { … }`. Same-name blocks reopen-merge;
|
|
198
210
|
* `span` points at the first opening.
|
|
211
|
+
*
|
|
212
|
+
* Entities are stored canonically (ADR 224) in `entries[kind][name]`, where
|
|
213
|
+
* `kind` is the PSL keyword for built-ins or the block discriminator for
|
|
214
|
+
* extension kinds, e.g. `entries['policy_select']['ReadPosts']`.
|
|
199
215
|
*/
|
|
200
216
|
export interface PslNamespace {
|
|
201
217
|
readonly kind: 'namespace';
|
|
202
218
|
readonly name: string;
|
|
219
|
+
/** Canonical store: a frozen container of frozen per-kind maps. The accessors below derive from it. */
|
|
220
|
+
readonly entries: Readonly<Record<string, Readonly<Record<string, PslNamespaceEntry>>>>;
|
|
221
|
+
/** Built-in models, from `entries['model']`. Extension kinds: {@link namespacePslExtensionBlocks}. */
|
|
203
222
|
readonly models: readonly PslModel[];
|
|
223
|
+
/** Built-in enums, from `entries['enum']`. */
|
|
204
224
|
readonly enums: readonly PslEnum[];
|
|
225
|
+
/** Built-in composite types, from `entries['compositeType']`. */
|
|
205
226
|
readonly compositeTypes: readonly PslCompositeType[];
|
|
206
227
|
readonly span: PslSpan;
|
|
207
228
|
}
|
|
208
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Stores `entries`; exposes `models`/`enums`/`compositeTypes` as getters over
|
|
232
|
+
* it. The getters are prototype members (non-enumerable), so spreading or
|
|
233
|
+
* `JSON.stringify`-ing a namespace copies only `entries`, never a duplicate view.
|
|
234
|
+
*/
|
|
235
|
+
class PslNamespaceNode implements PslNamespace {
|
|
236
|
+
readonly kind = 'namespace' as const;
|
|
237
|
+
readonly name: string;
|
|
238
|
+
readonly entries: Readonly<Record<string, Readonly<Record<string, PslNamespaceEntry>>>>;
|
|
239
|
+
readonly span: PslSpan;
|
|
240
|
+
|
|
241
|
+
constructor(init: {
|
|
242
|
+
readonly name: string;
|
|
243
|
+
readonly entries: Readonly<Record<string, Readonly<Record<string, PslNamespaceEntry>>>>;
|
|
244
|
+
readonly span: PslSpan;
|
|
245
|
+
}) {
|
|
246
|
+
this.name = init.name;
|
|
247
|
+
this.entries = init.entries;
|
|
248
|
+
this.span = init.span;
|
|
249
|
+
Object.freeze(this);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
get models(): readonly PslModel[] {
|
|
253
|
+
return blindCast<readonly PslModel[], 'entries[model] holds only PslModel by construction'>(
|
|
254
|
+
Object.values(this.entries['model'] ?? {}),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
get enums(): readonly PslEnum[] {
|
|
259
|
+
return blindCast<readonly PslEnum[], 'entries[enum] holds only PslEnum by construction'>(
|
|
260
|
+
Object.values(this.entries['enum'] ?? {}),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
get compositeTypes(): readonly PslCompositeType[] {
|
|
265
|
+
return blindCast<
|
|
266
|
+
readonly PslCompositeType[],
|
|
267
|
+
'entries[compositeType] holds only PslCompositeType by construction'
|
|
268
|
+
>(Object.values(this.entries['compositeType'] ?? {}));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Constructs a {@link PslNamespace}. Use this, never a namespace literal — the accessors must derive from `entries`. */
|
|
273
|
+
export function makePslNamespace(init: {
|
|
274
|
+
readonly kind: 'namespace';
|
|
275
|
+
readonly name: string;
|
|
276
|
+
readonly entries: Readonly<Record<string, Readonly<Record<string, PslNamespaceEntry>>>>;
|
|
277
|
+
readonly span: PslSpan;
|
|
278
|
+
}): PslNamespace {
|
|
279
|
+
return new PslNamespaceNode(init);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Builds the frozen `entries[kind][name]` container from per-kind arrays.
|
|
284
|
+
* Built-ins key on their PSL keyword; extension blocks key on their `kind`
|
|
285
|
+
* discriminator. Call this rather than hand-building the literal.
|
|
286
|
+
*/
|
|
287
|
+
export function makePslNamespaceEntries(
|
|
288
|
+
models: readonly PslModel[],
|
|
289
|
+
enums: readonly PslEnum[],
|
|
290
|
+
compositeTypes: readonly PslCompositeType[],
|
|
291
|
+
extensionBlocks: readonly PslExtensionBlock[],
|
|
292
|
+
): Readonly<Record<string, Readonly<Record<string, PslNamespaceEntry>>>> {
|
|
293
|
+
const container: Record<string, Readonly<Record<string, PslNamespaceEntry>>> = {};
|
|
294
|
+
|
|
295
|
+
if (models.length > 0) {
|
|
296
|
+
const map: Record<string, PslModel> = {};
|
|
297
|
+
for (const m of models) {
|
|
298
|
+
map[m.name] = m;
|
|
299
|
+
}
|
|
300
|
+
container['model'] = Object.freeze(map);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (enums.length > 0) {
|
|
304
|
+
const map: Record<string, PslEnum> = {};
|
|
305
|
+
for (const e of enums) {
|
|
306
|
+
map[e.name] = e;
|
|
307
|
+
}
|
|
308
|
+
container['enum'] = Object.freeze(map);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (compositeTypes.length > 0) {
|
|
312
|
+
const map: Record<string, PslCompositeType> = {};
|
|
313
|
+
for (const ct of compositeTypes) {
|
|
314
|
+
map[ct.name] = ct;
|
|
315
|
+
}
|
|
316
|
+
container['compositeType'] = Object.freeze(map);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const block of extensionBlocks) {
|
|
320
|
+
const existing = container[block.kind];
|
|
321
|
+
const newMap: Record<string, PslExtensionBlock> = existing
|
|
322
|
+
? blindCast<Record<string, PslExtensionBlock>, 'kind map holds only PslExtensionBlock'>({
|
|
323
|
+
...existing,
|
|
324
|
+
})
|
|
325
|
+
: {};
|
|
326
|
+
newMap[block.name] = block;
|
|
327
|
+
container[block.kind] = Object.freeze(newMap);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return Object.freeze(container);
|
|
331
|
+
}
|
|
332
|
+
|
|
209
333
|
export interface PslDocumentAst {
|
|
210
334
|
readonly kind: 'document';
|
|
211
335
|
readonly sourceId: string;
|
|
@@ -219,26 +343,102 @@ export interface PslDocumentAst {
|
|
|
219
343
|
* for consumers that don't (yet) need namespace-awareness.
|
|
220
344
|
*/
|
|
221
345
|
export function flatPslModels(ast: PslDocumentAst): readonly PslModel[] {
|
|
222
|
-
return ast.namespaces.flatMap((ns) =>
|
|
346
|
+
return ast.namespaces.flatMap((ns) =>
|
|
347
|
+
blindCast<PslModel[], 'model kind map contains only PslModel by construction'>(
|
|
348
|
+
Object.values(ns.entries['model'] ?? {}),
|
|
349
|
+
),
|
|
350
|
+
);
|
|
223
351
|
}
|
|
224
352
|
|
|
225
353
|
/**
|
|
226
354
|
* Returns all enums from every namespace in document order.
|
|
227
355
|
*/
|
|
228
356
|
export function flatPslEnums(ast: PslDocumentAst): readonly PslEnum[] {
|
|
229
|
-
return ast.namespaces.flatMap((ns) =>
|
|
357
|
+
return ast.namespaces.flatMap((ns) =>
|
|
358
|
+
blindCast<PslEnum[], 'enum kind map contains only PslEnum by construction'>(
|
|
359
|
+
Object.values(ns.entries['enum'] ?? {}),
|
|
360
|
+
),
|
|
361
|
+
);
|
|
230
362
|
}
|
|
231
363
|
|
|
232
364
|
/**
|
|
233
365
|
* Returns all composite types from every namespace in document order.
|
|
234
366
|
*/
|
|
235
367
|
export function flatPslCompositeTypes(ast: PslDocumentAst): readonly PslCompositeType[] {
|
|
236
|
-
return ast.namespaces.flatMap((ns) =>
|
|
368
|
+
return ast.namespaces.flatMap((ns) =>
|
|
369
|
+
blindCast<
|
|
370
|
+
PslCompositeType[],
|
|
371
|
+
'compositeType kind map contains only PslCompositeType by construction'
|
|
372
|
+
>(Object.values(ns.entries['compositeType'] ?? {})),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The set of `entries` kind keys that the framework parser reserves for
|
|
378
|
+
* built-in PSL entity kinds. Any own-enumerable key on `PslNamespace.entries`
|
|
379
|
+
* that is **not** in this set was contributed by an extension-block descriptor.
|
|
380
|
+
*
|
|
381
|
+
* Built-in keys match the PSL keyword used on each block type:
|
|
382
|
+
* `'model'`, `'enum'`, `'compositeType'`.
|
|
383
|
+
*/
|
|
384
|
+
export const BUILTIN_PSL_KIND_KEYS: ReadonlySet<string> = new Set([
|
|
385
|
+
'model',
|
|
386
|
+
'enum',
|
|
387
|
+
'compositeType',
|
|
388
|
+
]);
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Returns all extension-contributed blocks in the given namespace, in
|
|
392
|
+
* insertion order (the order the parser encountered them in the source).
|
|
393
|
+
*
|
|
394
|
+
* Reads from `namespace.entries`, skipping the three built-in kind keys
|
|
395
|
+
* (`'model'`, `'enum'`, `'compositeType'`). All remaining kind maps contain
|
|
396
|
+
* only `PslExtensionBlock` nodes by construction (see `makePslNamespaceEntries`).
|
|
397
|
+
*/
|
|
398
|
+
export function namespacePslExtensionBlocks(ns: PslNamespace): readonly PslExtensionBlock[] {
|
|
399
|
+
const result: PslExtensionBlock[] = [];
|
|
400
|
+
for (const [kindKey, kindMap] of Object.entries(ns.entries)) {
|
|
401
|
+
if (BUILTIN_PSL_KIND_KEYS.has(kindKey)) continue;
|
|
402
|
+
for (const entry of Object.values(kindMap)) {
|
|
403
|
+
result.push(
|
|
404
|
+
blindCast<
|
|
405
|
+
PslExtensionBlock,
|
|
406
|
+
'non-builtin kind maps contain only PslExtensionBlock by construction'
|
|
407
|
+
>(entry),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
237
412
|
}
|
|
238
413
|
|
|
239
414
|
export interface ParsePslDocumentInput {
|
|
240
415
|
readonly schema: string;
|
|
241
416
|
readonly sourceId: string;
|
|
417
|
+
/**
|
|
418
|
+
* Registry of declarative block descriptors, keyed by arbitrary path
|
|
419
|
+
* segments with {@link AuthoringPslBlockDescriptor} leaves. The registry
|
|
420
|
+
* teaches the parser which top-level keywords belong to extension
|
|
421
|
+
* contributions: when the parser encounters an unknown keyword, it looks
|
|
422
|
+
* it up here and, when found, reads the block generically into a
|
|
423
|
+
* {@link PslExtensionBlock} node. Absent or undefined means no extension
|
|
424
|
+
* blocks are registered and any unknown keyword yields
|
|
425
|
+
* `PSL_UNSUPPORTED_TOP_LEVEL_BLOCK`.
|
|
426
|
+
*
|
|
427
|
+
* Contrast with the parsed block nodes themselves, which live in
|
|
428
|
+
* {@link PslNamespace.entries} under their discriminator key (read them with
|
|
429
|
+
* {@link namespacePslExtensionBlocks}); this field holds the registry of
|
|
430
|
+
* descriptors that teach the parser how to read those blocks.
|
|
431
|
+
*/
|
|
432
|
+
readonly pslBlockDescriptors?: AuthoringPslBlockDescriptorNamespace;
|
|
433
|
+
/**
|
|
434
|
+
* Codec lookup for validating `value`-kind extension block parameters.
|
|
435
|
+
* When provided alongside `pslBlockDescriptors`, the generic validator runs
|
|
436
|
+
* over every parsed extension block after the full AST is assembled,
|
|
437
|
+
* appending any diagnostics to the parse result. Absent or undefined means
|
|
438
|
+
* no codec validation runs; `ref` resolution still runs when namespace
|
|
439
|
+
* context is available (built from the assembled namespaces).
|
|
440
|
+
*/
|
|
441
|
+
readonly codecLookup?: CodecLookup;
|
|
242
442
|
}
|
|
243
443
|
|
|
244
444
|
export interface ParsePslDocumentResult {
|