@prisma-next/framework-components 0.11.0-dev.9 → 0.12.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/codec-BFOsuHKK.d.mts.map +1 -1
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs.map +1 -1
- package/dist/components.d.mts +33 -1
- package/dist/components.d.mts.map +1 -0
- package/dist/components.mjs +64 -1
- package/dist/components.mjs.map +1 -0
- package/dist/control.d.mts +85 -110
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +1 -15
- package/dist/control.mjs.map +1 -1
- package/dist/emission-types-CMv_053d.d.mts.map +1 -1
- package/dist/execution.d.mts.map +1 -1
- package/dist/execution.mjs.map +1 -1
- package/dist/framework-authoring-BPPe9C9D.d.mts.map +1 -1
- package/dist/framework-authoring-DcEZ5Lin.mjs.map +1 -1
- package/dist/framework-components-CuoUhyB5.d.mts.map +1 -1
- package/dist/framework-components-FdqmlGUj.mjs.map +1 -1
- package/dist/ir.d.mts +20 -10
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +22 -1
- package/dist/ir.mjs.map +1 -1
- package/dist/psl-ast-BDXL7iCg.d.mts.map +1 -1
- package/dist/psl-ast.mjs.map +1 -1
- package/dist/runtime.d.mts +11 -0
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +6 -2
- package/dist/runtime.mjs.map +1 -1
- package/dist/utils.d.mts.map +1 -1
- package/dist/utils.mjs.map +1 -1
- package/package.json +18 -7
- package/src/control/contract-serializer.ts +19 -0
- package/src/control/control-capabilities.ts +1 -26
- package/src/control/control-instances.ts +3 -3
- package/src/control/control-migration-types.ts +59 -108
- package/src/control/control-result-types.ts +7 -0
- package/src/execution/runtime-core.ts +13 -2
- package/src/execution/runtime-middleware.ts +11 -0
- package/src/exports/components.ts +1 -0
- package/src/exports/control.ts +2 -7
- package/src/exports/ir.ts +1 -0
- package/src/ir/domain.ts +23 -0
- package/src/ir/namespace.ts +4 -4
- package/src/ir/storage.ts +7 -5
- package/src/shared/capabilities.ts +90 -0
|
@@ -14,26 +14,12 @@ 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
16
|
import type { ControlDriverInstance, ControlFamilyInstance } from './control-instances';
|
|
17
|
+
import type { OperationContext } from './control-result-types';
|
|
17
18
|
|
|
18
19
|
// ============================================================================
|
|
19
20
|
// Migration Package Metadata
|
|
20
21
|
// ============================================================================
|
|
21
22
|
|
|
22
|
-
/**
|
|
23
|
-
* Planner provenance recorded inside {@link MigrationMetadata}.
|
|
24
|
-
*
|
|
25
|
-
* `used` / `applied` track which migration hints the planner consulted
|
|
26
|
-
* vs. which it actually applied during emission; `plannerVersion`
|
|
27
|
-
* pins the planner build that produced the migration so future
|
|
28
|
-
* verification passes can recognise plans authored against an older
|
|
29
|
-
* planner.
|
|
30
|
-
*/
|
|
31
|
-
export interface MigrationHints {
|
|
32
|
-
readonly used: readonly string[];
|
|
33
|
-
readonly applied: readonly string[];
|
|
34
|
-
readonly plannerVersion: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
23
|
/**
|
|
38
24
|
* In-memory migration metadata envelope. Every migration is
|
|
39
25
|
* content-addressed: the `migrationHash` is a hash over the metadata
|
|
@@ -63,8 +49,6 @@ export interface MigrationMetadata {
|
|
|
63
49
|
readonly migrationHash: string;
|
|
64
50
|
readonly from: string | null;
|
|
65
51
|
readonly to: string;
|
|
66
|
-
readonly hints: MigrationHints;
|
|
67
|
-
readonly labels: readonly string[];
|
|
68
52
|
/**
|
|
69
53
|
* Sorted, deduplicated list of `invariantId`s declared by the
|
|
70
54
|
* migration's data-transform ops. Always present; an empty array
|
|
@@ -216,11 +200,10 @@ export interface MigrationPlan {
|
|
|
216
200
|
/**
|
|
217
201
|
* Contract space this plan applies to. Runners cross-check
|
|
218
202
|
* `options.space` against `plan.spaceId` so the marker row gets keyed
|
|
219
|
-
* by the right space when applying via
|
|
203
|
+
* by the right space when applying via {@link MigrationRunner.execute}.
|
|
220
204
|
*
|
|
221
|
-
* Optional
|
|
222
|
-
*
|
|
223
|
-
* enforce that it matches `options.space`.
|
|
205
|
+
* Optional because not every plan carries a space id; when present,
|
|
206
|
+
* runners enforce that it matches `options.space`.
|
|
224
207
|
*/
|
|
225
208
|
readonly spaceId?: string;
|
|
226
209
|
/**
|
|
@@ -316,13 +299,25 @@ export type MigrationPlannerResult = MigrationPlannerSuccessResult | MigrationPl
|
|
|
316
299
|
// ============================================================================
|
|
317
300
|
|
|
318
301
|
/**
|
|
319
|
-
*
|
|
302
|
+
* Per-space success payload returned inside
|
|
303
|
+
* {@link MigrationRunnerSuccessValue.perSpaceResults}.
|
|
320
304
|
*/
|
|
321
|
-
export interface
|
|
305
|
+
export interface MigrationRunnerPerSpaceSuccessValue {
|
|
322
306
|
readonly operationsPlanned: number;
|
|
323
307
|
readonly operationsExecuted: number;
|
|
324
308
|
}
|
|
325
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Success value for migration runner execution across one or more contract
|
|
312
|
+
* spaces.
|
|
313
|
+
*/
|
|
314
|
+
export interface MigrationRunnerSuccessValue {
|
|
315
|
+
readonly perSpaceResults: ReadonlyArray<{
|
|
316
|
+
readonly space: string;
|
|
317
|
+
readonly value: MigrationRunnerPerSpaceSuccessValue;
|
|
318
|
+
}>;
|
|
319
|
+
}
|
|
320
|
+
|
|
326
321
|
/**
|
|
327
322
|
* Failure details for migration runner execution.
|
|
328
323
|
*/
|
|
@@ -335,6 +330,11 @@ export interface MigrationRunnerFailure {
|
|
|
335
330
|
readonly why?: string;
|
|
336
331
|
/** Optional metadata for debugging and UX (e.g., schema issues, SQL state). */
|
|
337
332
|
readonly meta?: Record<string, unknown>;
|
|
333
|
+
/**
|
|
334
|
+
* Identifier of the space whose plan caused the rollback when
|
|
335
|
+
* {@link MigrationRunner.execute} processes multiple spaces.
|
|
336
|
+
*/
|
|
337
|
+
readonly failingSpace?: string;
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
/**
|
|
@@ -445,64 +445,20 @@ export interface MigrationPlanner<
|
|
|
445
445
|
* @template TFamilyId - The family ID (e.g., 'sql', 'document')
|
|
446
446
|
* @template TTargetId - The target ID (e.g., 'postgres', 'mysql')
|
|
447
447
|
*/
|
|
448
|
-
export interface MigrationRunner<
|
|
449
|
-
TFamilyId extends string = string,
|
|
450
|
-
TTargetId extends string = string,
|
|
451
|
-
> {
|
|
452
|
-
/**
|
|
453
|
-
* Execute a migration plan against the configured driver.
|
|
454
|
-
*
|
|
455
|
-
* The `plan` parameter is trusted input. Callers are responsible for
|
|
456
|
-
* upstream verification of the originating migration package — typically
|
|
457
|
-
* by obtaining the package via `readMigrationPackage` from
|
|
458
|
-
* `@prisma-next/migration-tools/io`, which performs hash-integrity checks
|
|
459
|
-
* at the load boundary. Runners do not re-verify the plan and assume the
|
|
460
|
-
* `(metadata, ops)` pair on disk has not been tampered with since emit.
|
|
461
|
-
*/
|
|
462
|
-
execute(options: {
|
|
463
|
-
readonly plan: MigrationPlan;
|
|
464
|
-
readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
|
|
465
|
-
readonly destinationContract: unknown;
|
|
466
|
-
readonly policy: MigrationOperationPolicy;
|
|
467
|
-
readonly callbacks?: {
|
|
468
|
-
onOperationStart?(op: MigrationPlanOperation): void;
|
|
469
|
-
onOperationComplete?(op: MigrationPlanOperation): void;
|
|
470
|
-
};
|
|
471
|
-
/**
|
|
472
|
-
* Execution-time checks configuration.
|
|
473
|
-
* All checks default to `true` (enabled) when omitted.
|
|
474
|
-
*/
|
|
475
|
-
readonly executionChecks?: MigrationRunnerExecutionChecks;
|
|
476
|
-
/**
|
|
477
|
-
* Active framework components participating in this composition.
|
|
478
|
-
* Families/targets can interpret this list to derive family-specific metadata.
|
|
479
|
-
* All components must have matching familyId and targetId.
|
|
480
|
-
*/
|
|
481
|
-
readonly frameworkComponents: ReadonlyArray<
|
|
482
|
-
TargetBoundComponentDescriptor<TFamilyId, TTargetId>
|
|
483
|
-
>;
|
|
484
|
-
}): Promise<MigrationRunnerResult>;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// ============================================================================
|
|
488
|
-
// Multi-space runner protocol (extension contract spaces, TML-2397)
|
|
489
|
-
// ============================================================================
|
|
490
|
-
|
|
491
448
|
/**
|
|
492
|
-
* Per-space input for {@link
|
|
449
|
+
* Per-space input for {@link MigrationRunner.execute}.
|
|
493
450
|
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
451
|
+
* Each entry's `driver` must reference the same connection the outer
|
|
452
|
+
* transaction is opened on (typically the same value as the top-level
|
|
453
|
+
* `driver` on `execute`). An apply that targets one space passes a
|
|
454
|
+
* one-element `perSpaceOptions` list.
|
|
498
455
|
*
|
|
499
456
|
* Family-specific runners (e.g. the SQL family's `SqlMigrationRunner`) define
|
|
500
457
|
* a richer per-space option shape that is structurally compatible with this
|
|
501
|
-
* one — additional optional fields (e.g. SQL's `
|
|
502
|
-
*
|
|
503
|
-
* affecting cross-target wiring.
|
|
458
|
+
* one — additional optional fields (e.g. SQL's `schemaName`, `callbacks`) are
|
|
459
|
+
* tolerated by the underlying runner without affecting cross-target wiring.
|
|
504
460
|
*/
|
|
505
|
-
export interface
|
|
461
|
+
export interface MigrationRunnerPerSpaceOptions<
|
|
506
462
|
TFamilyId extends string = string,
|
|
507
463
|
TTargetId extends string = string,
|
|
508
464
|
> {
|
|
@@ -513,45 +469,40 @@ export interface MultiSpaceRunnerPerSpaceOptions<
|
|
|
513
469
|
readonly policy: MigrationOperationPolicy;
|
|
514
470
|
readonly executionChecks?: MigrationRunnerExecutionChecks;
|
|
515
471
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
|
|
472
|
+
/**
|
|
473
|
+
* When `false`, schema verification tolerates objects owned by sibling
|
|
474
|
+
* contract spaces. Aggregate apply passes `false` per space because each
|
|
475
|
+
* `destinationContract` describes only that space's slice.
|
|
476
|
+
*/
|
|
477
|
+
readonly strictVerification?: boolean;
|
|
478
|
+
/**
|
|
479
|
+
* Paths and metadata forwarded to schema verification diagnostics.
|
|
480
|
+
*/
|
|
481
|
+
readonly context?: OperationContext;
|
|
516
482
|
}
|
|
517
483
|
|
|
518
|
-
export interface
|
|
519
|
-
readonly perSpaceResults: ReadonlyArray<{
|
|
520
|
-
readonly space: string;
|
|
521
|
-
readonly value: MigrationRunnerSuccessValue;
|
|
522
|
-
}>;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
export interface MultiSpaceRunnerFailure extends MigrationRunnerFailure {
|
|
526
|
-
/** Identifier of the space whose plan caused the rollback. */
|
|
527
|
-
readonly failingSpace: string;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
export type MultiSpaceRunnerResult = Result<MultiSpaceRunnerSuccessValue, MultiSpaceRunnerFailure>;
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Optional capability for runners that can apply a list of per-space plans.
|
|
534
|
-
* Atomicity semantics differ by family:
|
|
535
|
-
*
|
|
536
|
-
* - SQL (`SqlMigrationRunner`) opens one outer transaction across every
|
|
537
|
-
* space; a failure on any space rolls back every space's writes.
|
|
538
|
-
* - Mongo (`mongoTargetDescriptor`) cannot wrap most DDL ops in a session
|
|
539
|
-
* transaction (TML-2408), so it iterates per-space without an outer
|
|
540
|
-
* transaction and relies on per-space-internal verify-gated marker
|
|
541
|
-
* atomicity. Earlier-advanced markers are not rolled back when a later
|
|
542
|
-
* space fails; re-running resumes from the failing space.
|
|
543
|
-
*
|
|
544
|
-
* The capability is declared at the framework layer so CLI utilities can
|
|
545
|
-
* route through it without importing any specific family directly.
|
|
546
|
-
*/
|
|
547
|
-
export interface MultiSpaceCapableRunner<
|
|
484
|
+
export interface MigrationRunner<
|
|
548
485
|
TFamilyId extends string = string,
|
|
549
486
|
TTargetId extends string = string,
|
|
550
487
|
> {
|
|
551
|
-
|
|
488
|
+
/**
|
|
489
|
+
* Apply one or more per-space migration plans against the configured driver.
|
|
490
|
+
*
|
|
491
|
+
* Each plan is trusted input. Callers are responsible for upstream
|
|
492
|
+
* verification of the originating migration package — typically by
|
|
493
|
+
* obtaining the package via `readMigrationPackage` from
|
|
494
|
+
* `@prisma-next/migration-tools/io`, which performs hash-integrity checks
|
|
495
|
+
* at the load boundary. Runners do not re-verify plans and assume the
|
|
496
|
+
* `(metadata, ops)` pairs on disk have not been tampered with since emit.
|
|
497
|
+
*
|
|
498
|
+
* Atomicity semantics differ by family: SQL targets open one outer
|
|
499
|
+
* transaction across every space; Mongo iterates per-space without an
|
|
500
|
+
* outer transaction and relies on per-space verify-gated marker atomicity.
|
|
501
|
+
*/
|
|
502
|
+
execute(options: {
|
|
552
503
|
readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
|
|
553
|
-
readonly perSpaceOptions: ReadonlyArray<
|
|
554
|
-
}): Promise<
|
|
504
|
+
readonly perSpaceOptions: ReadonlyArray<MigrationRunnerPerSpaceOptions<TFamilyId, TTargetId>>;
|
|
505
|
+
}): Promise<MigrationRunnerResult>;
|
|
555
506
|
}
|
|
556
507
|
|
|
557
508
|
// ============================================================================
|
|
@@ -85,6 +85,13 @@ export interface BaseSchemaIssue {
|
|
|
85
85
|
|
|
86
86
|
export interface EnumValuesChangedIssue {
|
|
87
87
|
readonly kind: 'enum_values_changed';
|
|
88
|
+
/**
|
|
89
|
+
* Namespace coordinate of the enum type that changed values. Populated by
|
|
90
|
+
* family verifiers that have the coordinate in scope when constructing the
|
|
91
|
+
* issue. Downstream planners trust this field as the authoritative subject
|
|
92
|
+
* coordinate and do not re-derive it by name lookup.
|
|
93
|
+
*/
|
|
94
|
+
readonly namespaceId: string;
|
|
88
95
|
readonly typeName: string;
|
|
89
96
|
readonly addedValues: readonly string[];
|
|
90
97
|
readonly removedValues: readonly string[];
|
|
@@ -119,6 +119,17 @@ export abstract class RuntimeCore<
|
|
|
119
119
|
// satisfy `signal?: AbortSignal`).
|
|
120
120
|
const codecCtx: CodecCallContext = signal === undefined ? {} : { signal };
|
|
121
121
|
|
|
122
|
+
// Per-execute middleware context. Spread the stored runtime-level
|
|
123
|
+
// template and mint a fresh `planExecutionId` so every hook in this
|
|
124
|
+
// call observes the same value, and two executions of the same plan
|
|
125
|
+
// observe distinct values. ADR 220. The same reference is threaded
|
|
126
|
+
// through `runBeforeExecuteChain` and `runWithMiddleware`; the plan
|
|
127
|
+
// itself flows through unchanged.
|
|
128
|
+
const execCtx: RuntimeMiddlewareContext = {
|
|
129
|
+
...self.ctx,
|
|
130
|
+
planExecutionId: crypto.randomUUID(),
|
|
131
|
+
};
|
|
132
|
+
|
|
122
133
|
async function* generator(): AsyncGenerator<Row, void, unknown> {
|
|
123
134
|
// Pre-check the signal at entry so an already-aborted caller observes
|
|
124
135
|
// RUNTIME.ABORTED on the first `next()` without any work being done.
|
|
@@ -130,13 +141,13 @@ export abstract class RuntimeCore<
|
|
|
130
141
|
// plan before opening the row source. Families that need
|
|
131
142
|
// pre-encode mutator visibility (SQL) override `execute` to
|
|
132
143
|
// inject the same chain at the equivalent point.
|
|
133
|
-
await runBeforeExecuteChain<TExec>(exec, self.middleware,
|
|
144
|
+
await runBeforeExecuteChain<TExec>(exec, self.middleware, execCtx);
|
|
134
145
|
// The driver yields raw `Record<string, unknown>`; we cast to `Row` here.
|
|
135
146
|
// The Row contract is enforced by the caller via `plan._row`.
|
|
136
147
|
yield* runWithMiddleware<TExec, Row>(
|
|
137
148
|
exec,
|
|
138
149
|
self.middleware,
|
|
139
|
-
|
|
150
|
+
execCtx,
|
|
140
151
|
() => self.runDriver(exec) as AsyncIterable<Row>,
|
|
141
152
|
);
|
|
142
153
|
}
|
|
@@ -79,6 +79,17 @@ export interface RuntimeMiddlewareContext {
|
|
|
79
79
|
* scope. Existing middleware that ignore the field are unaffected.
|
|
80
80
|
*/
|
|
81
81
|
readonly scope: 'runtime' | 'connection' | 'transaction';
|
|
82
|
+
/**
|
|
83
|
+
* Identity for one `execute()` call. The runtime mints a fresh value via
|
|
84
|
+
* `crypto.randomUUID()` when it constructs the per-execute context, and
|
|
85
|
+
* the same context reference is threaded through every middleware phase
|
|
86
|
+
* (`beforeExecute`, `intercept`, `onRow`, `afterExecute`). Every hook in
|
|
87
|
+
* one execute call therefore observes the same `planExecutionId`; two
|
|
88
|
+
* executions of the same plan observe distinct values. Use this to
|
|
89
|
+
* correlate observations across the lifecycle of a single execute call
|
|
90
|
+
* (tracing, timing, audit). See ADR 220.
|
|
91
|
+
*/
|
|
92
|
+
readonly planExecutionId: string;
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
export interface AfterExecuteResult {
|
package/src/exports/control.ts
CHANGED
|
@@ -8,7 +8,6 @@ export type {
|
|
|
8
8
|
} from '../control/control-capabilities';
|
|
9
9
|
export {
|
|
10
10
|
hasMigrations,
|
|
11
|
-
hasMultiSpaceRunner,
|
|
12
11
|
hasOperationPreview,
|
|
13
12
|
hasPslContractInfer,
|
|
14
13
|
hasSchemaView,
|
|
@@ -28,7 +27,6 @@ export type {
|
|
|
28
27
|
ControlTargetInstance,
|
|
29
28
|
} from '../control/control-instances';
|
|
30
29
|
export type {
|
|
31
|
-
MigrationHints,
|
|
32
30
|
MigrationMetadata,
|
|
33
31
|
MigrationOperationClass,
|
|
34
32
|
MigrationOperationPolicy,
|
|
@@ -43,14 +41,11 @@ export type {
|
|
|
43
41
|
MigrationRunner,
|
|
44
42
|
MigrationRunnerExecutionChecks,
|
|
45
43
|
MigrationRunnerFailure,
|
|
44
|
+
MigrationRunnerPerSpaceOptions,
|
|
45
|
+
MigrationRunnerPerSpaceSuccessValue,
|
|
46
46
|
MigrationRunnerResult,
|
|
47
47
|
MigrationRunnerSuccessValue,
|
|
48
48
|
MigrationScaffoldContext,
|
|
49
|
-
MultiSpaceCapableRunner,
|
|
50
|
-
MultiSpaceRunnerFailure,
|
|
51
|
-
MultiSpaceRunnerPerSpaceOptions,
|
|
52
|
-
MultiSpaceRunnerResult,
|
|
53
|
-
MultiSpaceRunnerSuccessValue,
|
|
54
49
|
OpFactoryCall,
|
|
55
50
|
SerializedQueryPlan,
|
|
56
51
|
TargetMigrationsCapability,
|
package/src/exports/ir.ts
CHANGED
package/src/ir/domain.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ApplicationDomain } from '@prisma-next/contract/types';
|
|
2
|
+
import type { EntityCoordinate } from './storage';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lazy walk over every named domain entity in a {@link ApplicationDomain},
|
|
6
|
+
* yielded as {@link EntityCoordinate} tuples with `plane: 'domain'`.
|
|
7
|
+
*
|
|
8
|
+
* Same structural rules as {@link elementCoordinates} over storage: skip
|
|
9
|
+
* scalar `id`; each other object-valued property is an entity-kind slot.
|
|
10
|
+
*/
|
|
11
|
+
export function* domainElementCoordinates(
|
|
12
|
+
domain: Pick<ApplicationDomain, 'namespaces'>,
|
|
13
|
+
): Generator<EntityCoordinate> {
|
|
14
|
+
for (const [namespaceId, ns] of Object.entries(domain.namespaces)) {
|
|
15
|
+
for (const [entityKind, slot] of Object.entries(ns)) {
|
|
16
|
+
if (entityKind === 'id') continue;
|
|
17
|
+
if (slot === null || typeof slot !== 'object') continue;
|
|
18
|
+
for (const entityName of Object.keys(slot)) {
|
|
19
|
+
yield { plane: 'domain', namespaceId, entityKind, entityName };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/ir/namespace.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StorageNamespace } from '@prisma-next/contract/types';
|
|
1
2
|
import { type IRNode, IRNodeBase } from './ir-node';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -44,8 +45,8 @@ export const UNBOUND_NAMESPACE_ID = '__unbound__' as const;
|
|
|
44
45
|
* own native idiom). Generic consumers walking "all named entries" go
|
|
45
46
|
* through a family-typed namespace, not the framework `Namespace`.
|
|
46
47
|
*
|
|
47
|
-
* Every namespace concretion (e.g.
|
|
48
|
-
* `
|
|
48
|
+
* Every namespace concretion (e.g. family-built SQL namespaces,
|
|
49
|
+
* `MongoUnboundNamespace`, target-promoted namespaces like
|
|
49
50
|
* `PostgresSchema`) carries exactly: `id` (enumerable string), `kind`
|
|
50
51
|
* (non-enumerable string discriminator set via `Object.defineProperty`),
|
|
51
52
|
* and one or more entity-kind slot maps — each an own-enumerable property
|
|
@@ -57,8 +58,7 @@ export const UNBOUND_NAMESPACE_ID = '__unbound__' as const;
|
|
|
57
58
|
* on this invariant to enumerate entities structurally without
|
|
58
59
|
* family-specific knowledge.
|
|
59
60
|
*/
|
|
60
|
-
export interface Namespace extends IRNode {
|
|
61
|
-
readonly id: string;
|
|
61
|
+
export interface Namespace extends IRNode, StorageNamespace {
|
|
62
62
|
readonly kind: string;
|
|
63
63
|
}
|
|
64
64
|
|
package/src/ir/storage.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StorageBase } from '@prisma-next/contract/types';
|
|
1
2
|
import type { IRNode } from './ir-node';
|
|
2
3
|
import type { Namespace } from './namespace';
|
|
3
4
|
|
|
@@ -5,10 +6,9 @@ import type { Namespace } from './namespace';
|
|
|
5
6
|
* Canonical address for a named entity in Contract IR / Schema IR.
|
|
6
7
|
*
|
|
7
8
|
* `plane` is `'domain' | 'storage'`: which top-level contract plane the
|
|
8
|
-
* entity lives on. Domain-side walks
|
|
9
|
-
*
|
|
10
|
-
* `plane: 'storage'`.
|
|
11
|
-
* yet — domain-plane content lands in S1.C; the sibling walk lands there.
|
|
9
|
+
* entity lives on. Domain-side walks yield `plane: 'domain'` via
|
|
10
|
+
* {@link domainElementCoordinates}; {@link elementCoordinates} over storage
|
|
11
|
+
* yields `plane: 'storage'`.
|
|
12
12
|
*
|
|
13
13
|
* Cross-plane references obey a directional invariant: domain → storage is
|
|
14
14
|
* allowed; storage → domain is forbidden. That rule is enforced by a
|
|
@@ -36,7 +36,9 @@ export interface EntityCoordinate {
|
|
|
36
36
|
* property whose value is a non-null object, yields one coordinate per
|
|
37
37
|
* entry key in that map. No family-specific slot vocabulary is required.
|
|
38
38
|
*/
|
|
39
|
-
export function* elementCoordinates(
|
|
39
|
+
export function* elementCoordinates(
|
|
40
|
+
storage: Pick<StorageBase, 'namespaces'>,
|
|
41
|
+
): Generator<EntityCoordinate> {
|
|
40
42
|
for (const [namespaceId, ns] of Object.entries(storage.namespaces)) {
|
|
41
43
|
for (const [entityKind, slot] of Object.entries(ns)) {
|
|
42
44
|
if (entityKind === 'id') continue;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability matrix merge primitive shared by emit-time and runtime stack composition.
|
|
3
|
+
*
|
|
4
|
+
* The CLI's `enrichContract` and the SQL runtime's `createExecutionContext` both need
|
|
5
|
+
* to fold a stack of component descriptors' `capabilities` declarations into a single
|
|
6
|
+
* matrix keyed by namespace. Keeping the primitive here lets both call sites stay
|
|
7
|
+
* byte-for-byte consistent without one depending on the other.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
11
|
+
|
|
12
|
+
type CapabilityMatrix = Record<string, Record<string, boolean>>;
|
|
13
|
+
|
|
14
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sortDeep(value: unknown): unknown {
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return value.map(sortDeep);
|
|
21
|
+
}
|
|
22
|
+
if (!isPlainObject(value)) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
26
|
+
const next: Record<string, unknown> = {};
|
|
27
|
+
for (const [key, child] of entries) {
|
|
28
|
+
next[key] = sortDeep(child);
|
|
29
|
+
}
|
|
30
|
+
return next;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractCapabilityMatrix(value: unknown): CapabilityMatrix {
|
|
34
|
+
if (!isPlainObject(value)) return {};
|
|
35
|
+
|
|
36
|
+
const out: CapabilityMatrix = {};
|
|
37
|
+
for (const [namespace, maybeCaps] of Object.entries(value)) {
|
|
38
|
+
if (!isPlainObject(maybeCaps)) continue;
|
|
39
|
+
const caps: Record<string, boolean> = {};
|
|
40
|
+
for (const [key, flag] of Object.entries(maybeCaps)) {
|
|
41
|
+
if (typeof flag === 'boolean') {
|
|
42
|
+
caps[key] = flag;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (Object.keys(caps).length > 0) {
|
|
46
|
+
out[namespace] = caps;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Merge an ordered list of contributor capability declarations into a base matrix.
|
|
55
|
+
*
|
|
56
|
+
* Behaviour:
|
|
57
|
+
* - `base` and each contributor's `capabilities` are filtered through the same
|
|
58
|
+
* structural extraction: non-plain-object namespace blocks are dropped,
|
|
59
|
+
* non-boolean leaves inside a namespace block are dropped, and a namespace
|
|
60
|
+
* block that ends up with zero boolean leaves is omitted entirely (so a
|
|
61
|
+
* later contributor with a malformed namespace cannot erase a namespace
|
|
62
|
+
* already present in `base`).
|
|
63
|
+
* - Non-plain-object `capabilities` on a contributor (including `undefined`,
|
|
64
|
+
* `null`, arrays, primitives) are skipped silently — the contributor
|
|
65
|
+
* contributes nothing.
|
|
66
|
+
* - Later contributors win on `(namespace, key)` collisions.
|
|
67
|
+
* - The returned object is fresh — neither `base` nor any contributor is mutated.
|
|
68
|
+
* - Output keys are sorted lexicographically at every plain-object level.
|
|
69
|
+
*/
|
|
70
|
+
export function mergeCapabilityMatrices(
|
|
71
|
+
base: Record<string, Record<string, boolean>>,
|
|
72
|
+
contributors: ReadonlyArray<{ readonly capabilities?: unknown }>,
|
|
73
|
+
): Record<string, Record<string, boolean>> {
|
|
74
|
+
const merged: CapabilityMatrix = extractCapabilityMatrix(base);
|
|
75
|
+
|
|
76
|
+
for (const contributor of contributors) {
|
|
77
|
+
const extracted = extractCapabilityMatrix(contributor.capabilities);
|
|
78
|
+
for (const [namespace, capabilities] of Object.entries(extracted)) {
|
|
79
|
+
merged[namespace] = {
|
|
80
|
+
...(merged[namespace] ?? {}),
|
|
81
|
+
...capabilities,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return blindCast<
|
|
87
|
+
CapabilityMatrix,
|
|
88
|
+
"sortDeep preserves the matrix shape but the recursive generic relationship can't be expressed to TS"
|
|
89
|
+
>(sortDeep(merged));
|
|
90
|
+
}
|