@prisma-next/framework-components 0.8.0 → 0.9.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/README.md +1 -1
- package/dist/authoring.d.mts +2 -2
- package/dist/authoring.mjs +2 -2
- package/dist/{codec-DcjlJbcO.d.mts → codec-BFOsuHKK.d.mts} +3 -3
- package/dist/{codec-DcjlJbcO.d.mts.map → codec-BFOsuHKK.d.mts.map} +1 -1
- package/dist/codec.d.mts +1 -1
- package/dist/codec.mjs.map +1 -1
- package/dist/components.d.mts +1 -1
- package/dist/control.d.mts +131 -31
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +9 -6
- package/dist/control.mjs.map +1 -1
- package/dist/execution.d.mts +1 -1
- package/dist/{framework-authoring-DGIQbNPt.d.mts → framework-authoring-Bb_GEnzj.d.mts} +43 -3
- package/dist/framework-authoring-Bb_GEnzj.d.mts.map +1 -0
- package/dist/{framework-authoring-DxXcjyJX.mjs → framework-authoring-DcEZ5Lin.mjs} +26 -5
- package/dist/framework-authoring-DcEZ5Lin.mjs.map +1 -0
- package/dist/{framework-components-DwkHnl-e.d.mts → framework-components-DHhNhWFR.d.mts} +3 -3
- package/dist/{framework-components-DwkHnl-e.d.mts.map → framework-components-DHhNhWFR.d.mts.map} +1 -1
- package/dist/ir.d.mts +161 -0
- package/dist/ir.d.mts.map +1 -0
- package/dist/ir.mjs +39 -0
- package/dist/ir.mjs.map +1 -0
- package/dist/runtime.d.mts +1 -1
- package/package.json +8 -7
- package/src/control/contract-serializer.ts +37 -0
- package/src/control/control-capabilities.ts +1 -1
- package/src/control/control-descriptors.ts +10 -0
- package/src/control/control-instances.ts +16 -16
- package/src/control/control-migration-types.ts +10 -4
- package/src/control/control-schema-view.ts +3 -3
- package/src/control/control-spaces.ts +2 -3
- package/src/control/control-stack.ts +24 -10
- package/src/control/schema-verifier.ts +51 -0
- package/src/exports/authoring.ts +7 -0
- package/src/exports/control.ts +7 -1
- package/src/exports/ir.ts +6 -0
- package/src/ir/ir-node.ts +62 -0
- package/src/ir/namespace.ts +40 -0
- package/src/ir/storage-type.ts +23 -0
- package/src/ir/storage.ts +41 -0
- package/src/shared/codec-types.ts +1 -1
- package/src/shared/codec.ts +1 -1
- package/src/shared/framework-authoring.ts +116 -12
- package/dist/framework-authoring-DGIQbNPt.d.mts.map +0 -1
- package/dist/framework-authoring-DxXcjyJX.mjs.map +0 -1
package/dist/ir.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/ir/ir-node.ts
|
|
2
|
+
var IRNodeBase = class {};
|
|
3
|
+
/**
|
|
4
|
+
* Seal an IR class instance after its constructor has assigned all
|
|
5
|
+
* fields. The free-helper form (rather than a `protected freeze()`
|
|
6
|
+
* instance method) keeps the class type structurally narrow so emitted
|
|
7
|
+
* contract literal types remain assignable to their class types.
|
|
8
|
+
*
|
|
9
|
+
* The helper name stays `freezeNode` — it operates on IR nodes
|
|
10
|
+
* regardless of root naming.
|
|
11
|
+
*/
|
|
12
|
+
function freezeNode(node) {
|
|
13
|
+
Object.freeze(node);
|
|
14
|
+
return node;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/ir/namespace.ts
|
|
18
|
+
/**
|
|
19
|
+
* Reserved sentinel namespace id meaning
|
|
20
|
+
* "no namespace bound at authoring time; resolve from connection context".
|
|
21
|
+
*
|
|
22
|
+
* Materialised target-side as a singleton subclass of the target's
|
|
23
|
+
* `NamespaceBase` concretion that overrides the namespace's
|
|
24
|
+
* qualifier-emission methods to elide the prefix entirely. Call sites
|
|
25
|
+
* stay polymorphic and never branch on `id === UNSPECIFIED_NAMESPACE_ID`
|
|
26
|
+
* — the singleton's overrides drop the qualifier so emitted SQL / Mongo
|
|
27
|
+
* commands look unqualified.
|
|
28
|
+
*
|
|
29
|
+
* Encoded as an exported const (rather than scattered string literals)
|
|
30
|
+
* so the sentinel-id invariant is single-sourced: any production-source
|
|
31
|
+
* site that constructs an unspecified-namespace singleton imports this
|
|
32
|
+
* constant.
|
|
33
|
+
*/
|
|
34
|
+
const UNSPECIFIED_NAMESPACE_ID = "__unspecified__";
|
|
35
|
+
var NamespaceBase = class extends IRNodeBase {};
|
|
36
|
+
//#endregion
|
|
37
|
+
export { IRNodeBase, NamespaceBase, UNSPECIFIED_NAMESPACE_ID, freezeNode };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=ir.mjs.map
|
package/dist/ir.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ir.mjs","names":[],"sources":["../src/ir/ir-node.ts","../src/ir/namespace.ts"],"sourcesContent":["/**\n * Framework-level IR alphabet.\n *\n * The framework's contribution to Contract IR / Schema IR is a common\n * root for the IR class hierarchy and a freeze affordance. Family\n * abstract bases (e.g. `SqlNode`, `MongoSchemaIRNode`) refine the alphabet\n * for their family shape; targets ship the concrete classes.\n *\n * `kind` is an optional discriminator on the base. Families and leaves\n * that benefit from discriminated-union dispatch declare their own\n * literal `kind` at the level that earns it — Mongo leaves carry\n * per-class literals (`readonly kind = 'mongo-collection' as const`)\n * because Mongo IR has polymorphic walkers; SQL declares a single\n * family-level `kind = 'sql'` on `SqlNode` because SQL IR has no\n * polymorphic dispatch today. No framework consumer dispatches on\n * `IRNode.kind` at the BASE type — every dispatch site narrows\n * through a union of leaves where each leaf carries a literal kind, so\n * requiring `kind` at the base would be unearned. Future leaves that\n * earn polymorphic dispatch override with a required literal at that\n * leaf (e.g. `override readonly kind = 'postgres-enum' as const`).\n *\n * `IRNodeBase` carries no methods: the freeze-and-assign affordance\n * lives in the free `freezeNode` helper below. Keeping `freezeNode` out\n * of the class type means an emitted contract literal type\n * (`{ readonly kind: 'mongo-collection', ... }` or an unkeyed literal\n * like `{ nativeType, codecId, nullable }`) is structurally assignable\n * to its class type — a `protected freeze()` instance method would\n * otherwise leak into the public type surface and require the literal\n * to carry it too.\n *\n * Subclasses construct fields then call `freezeNode(this)` to seal the\n * instance. Frozen instances + plain readonly fields keep IR nodes\n * JSON-clean by construction, so `JSON.stringify(node)` produces canonical\n * JSON without a `toJSON()` method. The `ContractSerializer` SPI handles\n * round-trip from canonical JSON back to typed class instances.\n *\n * The name (`IRNode` / `IRNodeBase`) reflects the dual-hierarchy reality:\n * this base is the common root for both Contract IR and Schema IR class\n * hierarchies, not a Schema-IR-specific alphabet.\n */\n\nexport interface IRNode {\n readonly kind?: string;\n}\n\nexport abstract class IRNodeBase implements IRNode {\n abstract readonly kind?: string;\n}\n\n/**\n * Seal an IR class instance after its constructor has assigned all\n * fields. The free-helper form (rather than a `protected freeze()`\n * instance method) keeps the class type structurally narrow so emitted\n * contract literal types remain assignable to their class types.\n *\n * The helper name stays `freezeNode` — it operates on IR nodes\n * regardless of root naming.\n */\nexport function freezeNode<T extends IRNode>(node: T): T {\n Object.freeze(node);\n return node;\n}\n","import { type IRNode, IRNodeBase } from './ir-node';\n\n/**\n * Reserved sentinel namespace id meaning\n * \"no namespace bound at authoring time; resolve from connection context\".\n *\n * Materialised target-side as a singleton subclass of the target's\n * `NamespaceBase` concretion that overrides the namespace's\n * qualifier-emission methods to elide the prefix entirely. Call sites\n * stay polymorphic and never branch on `id === UNSPECIFIED_NAMESPACE_ID`\n * — the singleton's overrides drop the qualifier so emitted SQL / Mongo\n * commands look unqualified.\n *\n * Encoded as an exported const (rather than scattered string literals)\n * so the sentinel-id invariant is single-sourced: any production-source\n * site that constructs an unspecified-namespace singleton imports this\n * constant.\n */\nexport const UNSPECIFIED_NAMESPACE_ID = '__unspecified__' as const;\n\n/**\n * Framework-level building block for a \"namespace\" — the database-level\n * grouping under which storage objects (tables, collections, enums, …)\n * reside. Each target's namespace concretion maps the framework concept to\n * a target-native binding:\n *\n * - Postgres: a schema (`CREATE SCHEMA …`); rendered as `\"<schema>\"`.\n * - SQLite: the singleton `UNSPECIFIED_NAMESPACE_ID`; emitted SQL has no qualifier.\n * - Mongo: the connection's `db` field; addressed as a database name.\n *\n * See `UNSPECIFIED_NAMESPACE_ID` above for the sentinel id and the\n * singleton-subclass pattern that materialises it.\n */\nexport interface Namespace extends IRNode {\n readonly id: string;\n}\n\nexport abstract class NamespaceBase extends IRNodeBase implements Namespace {\n abstract readonly id: string;\n}\n"],"mappings":";AA6CA,IAAsB,aAAtB,MAAmD;;;;;;;;;;AAanD,SAAgB,WAA6B,MAAY;CACvD,OAAO,OAAO,KAAK;CACnB,OAAO;;;;;;;;;;;;;;;;;;;;AC1CT,MAAa,2BAA2B;AAmBxC,IAAsB,gBAAtB,cAA4C,WAAgC"}
|
package/dist/runtime.d.mts
CHANGED
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/framework-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-dev.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "Framework component types, assembly logic, and stack creation for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/contract": "0.
|
|
10
|
-
"@prisma-next/operations": "0.
|
|
11
|
-
"@prisma-next/ts-render": "0.
|
|
12
|
-
"@prisma-next/utils": "0.
|
|
9
|
+
"@prisma-next/contract": "0.9.0-dev.1",
|
|
10
|
+
"@prisma-next/operations": "0.9.0-dev.1",
|
|
11
|
+
"@prisma-next/ts-render": "0.9.0-dev.1",
|
|
12
|
+
"@prisma-next/utils": "0.9.0-dev.1",
|
|
13
13
|
"@standard-schema/spec": "^1.1.0"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@prisma-next/tsconfig": "0.
|
|
17
|
-
"@prisma-next/tsdown": "0.
|
|
16
|
+
"@prisma-next/tsconfig": "0.9.0-dev.1",
|
|
17
|
+
"@prisma-next/tsdown": "0.9.0-dev.1",
|
|
18
18
|
"tsdown": "0.22.0",
|
|
19
19
|
"typescript": "5.9.3",
|
|
20
20
|
"vitest": "4.1.5"
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"./control": "./dist/control.mjs",
|
|
31
31
|
"./emission": "./dist/emission.mjs",
|
|
32
32
|
"./execution": "./dist/execution.mjs",
|
|
33
|
+
"./ir": "./dist/ir.mjs",
|
|
33
34
|
"./psl-ast": "./dist/psl-ast.mjs",
|
|
34
35
|
"./runtime": "./dist/runtime.mjs",
|
|
35
36
|
"./utils": "./dist/utils.mjs",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { JsonObject } from '@prisma-next/utils/json';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Framework SPI for moving a contract between its canonical on-disk JSON
|
|
5
|
+
* form and its in-memory class-hierarchy form. Both directions live on the
|
|
6
|
+
* same SPI so the conceptual seam — "the boundary where the contract
|
|
7
|
+
* crosses between persisted JSON and live class instances" — has a single
|
|
8
|
+
* named home.
|
|
9
|
+
*
|
|
10
|
+
* Both faces are needed by the framework today (round-trip property tests,
|
|
11
|
+
* drift detection, future canonicalization), not eventually. For most
|
|
12
|
+
* targets `serializeContract` is identity over JSON-clean class instances;
|
|
13
|
+
* the method exists because the seam is real, not as a convention placeholder.
|
|
14
|
+
*
|
|
15
|
+
* Implementers compose this SPI as a named property on their target
|
|
16
|
+
* descriptor (`descriptor.contractSerializer`); the descriptor itself
|
|
17
|
+
* remains the aggregator of all per-target SPIs.
|
|
18
|
+
*/
|
|
19
|
+
export interface ContractSerializer<TContract> {
|
|
20
|
+
/**
|
|
21
|
+
* Validate the JSON shape and construct typed class instances. Throws on
|
|
22
|
+
* structural / domain / storage validation failures. Returns the typed
|
|
23
|
+
* contract on success.
|
|
24
|
+
*/
|
|
25
|
+
deserializeContract(json: unknown): TContract;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Serialize a typed contract to its canonical JSON shape. Returns
|
|
29
|
+
* `JsonObject` so callers can stringify, hash, or feed the result into
|
|
30
|
+
* another SPI without re-asserting JSON-cleanness. Targets whose contract
|
|
31
|
+
* fields are JSON-clean by construction return the contract unchanged
|
|
32
|
+
* (the symmetric pair to `deserializeContract`); targets that need to
|
|
33
|
+
* canonicalize on the way out (key ordering, dropping computed-only
|
|
34
|
+
* fields, normalizing numeric encodings) do that work here.
|
|
35
|
+
*/
|
|
36
|
+
serializeContract(contract: TContract): JsonObject;
|
|
37
|
+
}
|
|
@@ -83,7 +83,7 @@ export function hasOperationPreview<TFamilyId extends string, TSchemaIR>(
|
|
|
83
83
|
* extension-contract-spaces project spec — TML-2397).
|
|
84
84
|
*
|
|
85
85
|
* The CLI's shared `applyAggregate` primitive uses this guard so that
|
|
86
|
-
* `db init` / `db update` / `
|
|
86
|
+
* `db init` / `db update` / `migrate` route uniformly through one
|
|
87
87
|
* dispatch path regardless of family.
|
|
88
88
|
*/
|
|
89
89
|
export function hasMultiSpaceRunner<TFamilyId extends string, TTargetId extends string>(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
1
2
|
import type {
|
|
2
3
|
AdapterDescriptor,
|
|
3
4
|
DriverDescriptor,
|
|
@@ -5,6 +6,7 @@ import type {
|
|
|
5
6
|
FamilyDescriptor,
|
|
6
7
|
TargetDescriptor,
|
|
7
8
|
} from '../shared/framework-components';
|
|
9
|
+
import type { ContractSerializer } from './contract-serializer';
|
|
8
10
|
import type {
|
|
9
11
|
ControlAdapterInstance,
|
|
10
12
|
ControlDriverInstance,
|
|
@@ -33,7 +35,15 @@ export interface ControlTargetDescriptor<
|
|
|
33
35
|
TFamilyId,
|
|
34
36
|
TTargetId
|
|
35
37
|
>,
|
|
38
|
+
TContract extends Contract = Contract,
|
|
36
39
|
> extends TargetDescriptor<TFamilyId, TTargetId> {
|
|
40
|
+
/**
|
|
41
|
+
* JSON ⇄ class boundary for this target's contract. Every target
|
|
42
|
+
* ships the SPI: framework consumers reach the serializer through
|
|
43
|
+
* `descriptor.contractSerializer` rather than importing a per-target
|
|
44
|
+
* `deserializeContract` helper. The descriptor IS the aggregator.
|
|
45
|
+
*/
|
|
46
|
+
readonly contractSerializer: ContractSerializer<TContract>;
|
|
37
47
|
create(): TTargetInstance;
|
|
38
48
|
}
|
|
39
49
|
|
|
@@ -15,7 +15,15 @@ import type {
|
|
|
15
15
|
|
|
16
16
|
export interface ControlFamilyInstance<TFamilyId extends string, TSchemaIR>
|
|
17
17
|
extends FamilyInstance<TFamilyId> {
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The family seam-of-record for on-disk contract reads. Structurally
|
|
20
|
+
* validates the JSON envelope and hydrates IR-class instances via the
|
|
21
|
+
* per-target ContractSerializer. The single named entry point every
|
|
22
|
+
* CLI on-disk read crosses (TML-2536) — `as Contract` casts in
|
|
23
|
+
* production package sources are a serializer-bypass smell guarded by
|
|
24
|
+
* `pnpm lint:no-contract-cast`.
|
|
25
|
+
*/
|
|
26
|
+
deserializeContract(contractJson: unknown): Contract;
|
|
19
27
|
|
|
20
28
|
verify(options: {
|
|
21
29
|
readonly driver: ControlDriverInstance<TFamilyId, string>;
|
|
@@ -25,27 +33,19 @@ export interface ControlFamilyInstance<TFamilyId extends string, TSchemaIR>
|
|
|
25
33
|
readonly configPath?: string;
|
|
26
34
|
}): Promise<VerifyDatabaseResult>;
|
|
27
35
|
|
|
28
|
-
schemaVerify(options: {
|
|
29
|
-
readonly driver: ControlDriverInstance<TFamilyId, string>;
|
|
30
|
-
readonly contract: unknown;
|
|
31
|
-
readonly strict: boolean;
|
|
32
|
-
readonly contractPath: string;
|
|
33
|
-
readonly configPath?: string;
|
|
34
|
-
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, string>>;
|
|
35
|
-
}): Promise<VerifyDatabaseSchemaResult>;
|
|
36
|
-
|
|
37
36
|
/**
|
|
38
37
|
* Verify a contract against an already-introspected schema slice.
|
|
39
38
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* Callers that need to verify against the live database compose
|
|
40
|
+
* {@link introspect} + `verifySchema` directly; the family
|
|
41
|
+
* interface deliberately exposes the introspection step so callers
|
|
42
|
+
* can pre-project the schema (e.g. the aggregate verifier projects
|
|
43
|
+
* each member's claimed slice via
|
|
44
|
+
* {@link import('@prisma-next/migration-tools/aggregate').projectSchemaToSpace}).
|
|
45
45
|
*
|
|
46
46
|
* Synchronous — no I/O. Idempotent.
|
|
47
47
|
*/
|
|
48
|
-
|
|
48
|
+
verifySchema(options: {
|
|
49
49
|
readonly contract: unknown;
|
|
50
50
|
readonly schema: TSchemaIR;
|
|
51
51
|
readonly strict: boolean;
|
|
@@ -50,13 +50,19 @@ export interface MigrationHints {
|
|
|
50
50
|
* The on-disk JSON shape in `migration.json` matches this type
|
|
51
51
|
* field-for-field — `JSON.stringify(metadata, null, 2)` is the canonical
|
|
52
52
|
* writer output (defined in `@prisma-next/migration-tools/io`).
|
|
53
|
+
*
|
|
54
|
+
* The manifest carries identity (`from`, `to`, `migrationHash`) but
|
|
55
|
+
* not the full contract IRs themselves. The destination contract for a
|
|
56
|
+
* migration lives in the sibling `end-contract.json` on disk; the
|
|
57
|
+
* predecessor's contract lives in its own directory's `end-contract.json`.
|
|
58
|
+
* The runner depends only on `migration.json` + `ops.json` per package
|
|
59
|
+
* (plus the project-root / per-space `contract.json` head). See
|
|
60
|
+
* `docs/architecture docs/subsystems/7. Migration System.md`.
|
|
53
61
|
*/
|
|
54
62
|
export interface MigrationMetadata {
|
|
55
63
|
readonly migrationHash: string;
|
|
56
64
|
readonly from: string | null;
|
|
57
65
|
readonly to: string;
|
|
58
|
-
readonly fromContract: Contract | null;
|
|
59
|
-
readonly toContract: Contract;
|
|
60
66
|
readonly hints: MigrationHints;
|
|
61
67
|
readonly labels: readonly string[];
|
|
62
68
|
/**
|
|
@@ -394,8 +400,8 @@ export interface MigrationPlanner<
|
|
|
394
400
|
* Required at every call site to make the structural fact "I have a
|
|
395
401
|
* prior contract / I don't" visible in the type. Reconciliation
|
|
396
402
|
* commands (`db init`, `db update`) introspect a live schema and pass
|
|
397
|
-
* `null`; authoring commands (`migration plan`)
|
|
398
|
-
* bundle's `
|
|
403
|
+
* `null`; authoring commands (`migration plan`) load the previous
|
|
404
|
+
* bundle's `end-contract.json` from disk and pass the parsed value.
|
|
399
405
|
*/
|
|
400
406
|
readonly fromContract: Contract | null;
|
|
401
407
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* core view via the `toSchemaView` method on `FamilyInstance`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export type
|
|
11
|
+
export type SchemaViewNodeKind =
|
|
12
12
|
| 'root'
|
|
13
13
|
| 'namespace'
|
|
14
14
|
| 'collection'
|
|
@@ -22,7 +22,7 @@ export interface SchemaTreeVisitor<R> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface SchemaTreeNodeOptions {
|
|
25
|
-
readonly kind:
|
|
25
|
+
readonly kind: SchemaViewNodeKind;
|
|
26
26
|
readonly id: string;
|
|
27
27
|
readonly label: string;
|
|
28
28
|
readonly meta?: Record<string, unknown>;
|
|
@@ -30,7 +30,7 @@ export interface SchemaTreeNodeOptions {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export class SchemaTreeNode {
|
|
33
|
-
readonly kind:
|
|
33
|
+
readonly kind: SchemaViewNodeKind;
|
|
34
34
|
readonly id: string;
|
|
35
35
|
readonly label: string;
|
|
36
36
|
readonly meta?: Record<string, unknown>;
|
|
@@ -39,9 +39,8 @@ export interface ContractSpaceHeadRef {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Canonical structural shape of a migration package — the unit a planner
|
|
42
|
-
* produces and a runner consumes: a directory name, the
|
|
43
|
-
* envelope
|
|
44
|
-
* list.
|
|
42
|
+
* produces and a runner consumes: a directory name, the metadata
|
|
43
|
+
* envelope, and the operation list.
|
|
45
44
|
*
|
|
46
45
|
* In-memory by default. Readers in `@prisma-next/migration-tools`
|
|
47
46
|
* (`readMigrationPackage` / `readMigrationsDir`) return the augmented
|
|
@@ -2,11 +2,13 @@ import type { Codec } from '../shared/codec';
|
|
|
2
2
|
import type { CodecLookup, CodecMeta } from '../shared/codec-types';
|
|
3
3
|
import type {
|
|
4
4
|
AuthoringContributions,
|
|
5
|
+
AuthoringEntityTypeNamespace,
|
|
5
6
|
AuthoringFieldNamespace,
|
|
6
7
|
AuthoringTypeNamespace,
|
|
7
8
|
} from '../shared/framework-authoring';
|
|
8
9
|
import {
|
|
9
10
|
assertNoCrossRegistryCollisions,
|
|
11
|
+
isAuthoringEntityTypeDescriptor,
|
|
10
12
|
isAuthoringFieldPresetDescriptor,
|
|
11
13
|
isAuthoringTypeConstructorDescriptor,
|
|
12
14
|
mergeAuthoringNamespaces,
|
|
@@ -29,6 +31,7 @@ import type {
|
|
|
29
31
|
export interface AssembledAuthoringContributions {
|
|
30
32
|
readonly field: AuthoringFieldNamespace;
|
|
31
33
|
readonly type: AuthoringTypeNamespace;
|
|
34
|
+
readonly entityTypes: AuthoringEntityTypeNamespace;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export interface ControlStack<
|
|
@@ -147,6 +150,7 @@ export function assembleAuthoringContributions(
|
|
|
147
150
|
): AssembledAuthoringContributions {
|
|
148
151
|
const field = {} as Record<string, unknown>;
|
|
149
152
|
const type = {} as Record<string, unknown>;
|
|
153
|
+
const entityTypes = {} as Record<string, unknown>;
|
|
150
154
|
|
|
151
155
|
for (const descriptor of descriptors) {
|
|
152
156
|
if (descriptor.authoring?.field) {
|
|
@@ -158,25 +162,35 @@ export function assembleAuthoringContributions(
|
|
|
158
162
|
'field',
|
|
159
163
|
);
|
|
160
164
|
}
|
|
161
|
-
if (
|
|
162
|
-
|
|
165
|
+
if (descriptor.authoring?.type) {
|
|
166
|
+
mergeAuthoringNamespaces(
|
|
167
|
+
type,
|
|
168
|
+
descriptor.authoring.type,
|
|
169
|
+
[],
|
|
170
|
+
isAuthoringTypeConstructorDescriptor,
|
|
171
|
+
'type',
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (descriptor.authoring?.entityTypes) {
|
|
175
|
+
mergeAuthoringNamespaces(
|
|
176
|
+
entityTypes,
|
|
177
|
+
descriptor.authoring.entityTypes,
|
|
178
|
+
[],
|
|
179
|
+
isAuthoringEntityTypeDescriptor,
|
|
180
|
+
'entity',
|
|
181
|
+
);
|
|
163
182
|
}
|
|
164
|
-
mergeAuthoringNamespaces(
|
|
165
|
-
type,
|
|
166
|
-
descriptor.authoring.type,
|
|
167
|
-
[],
|
|
168
|
-
isAuthoringTypeConstructorDescriptor,
|
|
169
|
-
'type',
|
|
170
|
-
);
|
|
171
183
|
}
|
|
172
184
|
|
|
173
185
|
const fieldNamespace = field as AuthoringFieldNamespace;
|
|
174
186
|
const typeNamespace = type as AuthoringTypeNamespace;
|
|
175
|
-
|
|
187
|
+
const entityTypeNamespace = entityTypes as AuthoringEntityTypeNamespace;
|
|
188
|
+
assertNoCrossRegistryCollisions(typeNamespace, fieldNamespace, entityTypeNamespace);
|
|
176
189
|
|
|
177
190
|
return {
|
|
178
191
|
field: fieldNamespace,
|
|
179
192
|
type: typeNamespace,
|
|
193
|
+
entityTypes: entityTypeNamespace,
|
|
180
194
|
};
|
|
181
195
|
}
|
|
182
196
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { SchemaIssue } from './control-result-types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Framework SPI for verifying that an introspected schema matches the
|
|
5
|
+
* contract that authored it. The implementer walks the target's IR
|
|
6
|
+
* natively — concrete classes, target-only kinds — and reports issues.
|
|
7
|
+
*
|
|
8
|
+
* The framework verifier (per FR6) walks the contract-space aggregate,
|
|
9
|
+
* dispatches to the right target's verifier (`descriptor.schemaVerifier`)
|
|
10
|
+
* per space, and wraps the per-target results into a unified
|
|
11
|
+
* `VerifyDatabaseSchemaResult` with timings, summary, and per-node
|
|
12
|
+
* verification tree.
|
|
13
|
+
*
|
|
14
|
+
* Family-level abstract bases (e.g. `SqlSchemaVerifierBase`) carry the
|
|
15
|
+
* shared SQL/Mongo walk logic and expose protected hooks for target
|
|
16
|
+
* extensions; concrete target verifiers (`PostgresSchemaVerifier extends
|
|
17
|
+
* SqlSchemaVerifierBase`) own the dispatch on target-specific kinds.
|
|
18
|
+
*
|
|
19
|
+
* Target-specific issue kinds (RLS-policy-mismatch, namespace-mismatch,
|
|
20
|
+
* future function-shape-mismatch) are widened target-side; the framework
|
|
21
|
+
* `SchemaIssue` union (in `control-result-types.ts`) is intentionally not
|
|
22
|
+
* generic over target kinds in this project — see the spec's
|
|
23
|
+
* "Forward note: SchemaIssue layering" for the layering observation
|
|
24
|
+
* captured for a future project.
|
|
25
|
+
*/
|
|
26
|
+
export interface SchemaVerifier<TContract, TSchema> {
|
|
27
|
+
verifySchema(options: SchemaVerifyOptions<TContract, TSchema>): SchemaVerifyResult;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Minimal per-target verifier input. Family abstract bases extend this
|
|
32
|
+
* shape with family-specific options (`strict`, `frameworkComponents`,
|
|
33
|
+
* codec hooks, …) — the framework SPI itself stays at the contract +
|
|
34
|
+
* schema pair every implementer needs.
|
|
35
|
+
*/
|
|
36
|
+
export interface SchemaVerifyOptions<TContract, TSchema> {
|
|
37
|
+
readonly contract: TContract;
|
|
38
|
+
readonly schema: TSchema;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Per-target verifier result. The framework verifier wraps these into the
|
|
43
|
+
* existing `VerifyDatabaseSchemaResult` envelope (with timings, summary,
|
|
44
|
+
* per-node verification tree); the SPI itself returns just the
|
|
45
|
+
* core ok/issues pair so the seam between target-walk and
|
|
46
|
+
* framework-aggregation is explicit.
|
|
47
|
+
*/
|
|
48
|
+
export interface SchemaVerifyResult {
|
|
49
|
+
readonly ok: boolean;
|
|
50
|
+
readonly issues: readonly SchemaIssue[];
|
|
51
|
+
}
|
package/src/exports/authoring.ts
CHANGED
|
@@ -3,6 +3,11 @@ export type {
|
|
|
3
3
|
AuthoringArgumentDescriptor,
|
|
4
4
|
AuthoringColumnDefaultTemplate,
|
|
5
5
|
AuthoringContributions,
|
|
6
|
+
AuthoringEntityContext,
|
|
7
|
+
AuthoringEntityTypeDescriptor,
|
|
8
|
+
AuthoringEntityTypeFactoryOutput,
|
|
9
|
+
AuthoringEntityTypeNamespace,
|
|
10
|
+
AuthoringEntityTypeTemplateOutput,
|
|
6
11
|
AuthoringFieldNamespace,
|
|
7
12
|
AuthoringFieldPresetDescriptor,
|
|
8
13
|
AuthoringFieldPresetOutput,
|
|
@@ -14,9 +19,11 @@ export type {
|
|
|
14
19
|
export {
|
|
15
20
|
assertNoCrossRegistryCollisions,
|
|
16
21
|
hasRegisteredFieldNamespace,
|
|
22
|
+
instantiateAuthoringEntityType,
|
|
17
23
|
instantiateAuthoringFieldPreset,
|
|
18
24
|
instantiateAuthoringTypeConstructor,
|
|
19
25
|
isAuthoringArgRef,
|
|
26
|
+
isAuthoringEntityTypeDescriptor,
|
|
20
27
|
isAuthoringFieldPresetDescriptor,
|
|
21
28
|
isAuthoringTypeConstructorDescriptor,
|
|
22
29
|
mergeAuthoringNamespaces,
|
package/src/exports/control.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type { ImportRequirement } from '@prisma-next/ts-render';
|
|
2
|
+
export type { ContractSerializer } from '../control/contract-serializer';
|
|
2
3
|
export type {
|
|
3
4
|
MigratableTargetDescriptor,
|
|
4
5
|
OperationPreviewCapable,
|
|
@@ -78,9 +79,9 @@ export {
|
|
|
78
79
|
} from '../control/control-result-types';
|
|
79
80
|
export type {
|
|
80
81
|
CoreSchemaView,
|
|
81
|
-
SchemaNodeKind,
|
|
82
82
|
SchemaTreeNodeOptions,
|
|
83
83
|
SchemaTreeVisitor,
|
|
84
|
+
SchemaViewNodeKind,
|
|
84
85
|
} from '../control/control-schema-view';
|
|
85
86
|
export { SchemaTreeNode } from '../control/control-schema-view';
|
|
86
87
|
export type {
|
|
@@ -105,6 +106,11 @@ export {
|
|
|
105
106
|
extractComponentIds,
|
|
106
107
|
extractQueryOperationTypeImports,
|
|
107
108
|
} from '../control/control-stack';
|
|
109
|
+
export type {
|
|
110
|
+
SchemaVerifier,
|
|
111
|
+
SchemaVerifyOptions,
|
|
112
|
+
SchemaVerifyResult,
|
|
113
|
+
} from '../control/schema-verifier';
|
|
108
114
|
export type {
|
|
109
115
|
ControlMutationDefaultEntry,
|
|
110
116
|
ControlMutationDefaultRegistry,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { IRNode } from '../ir/ir-node';
|
|
2
|
+
export { freezeNode, IRNodeBase } from '../ir/ir-node';
|
|
3
|
+
export type { Namespace } from '../ir/namespace';
|
|
4
|
+
export { NamespaceBase, UNSPECIFIED_NAMESPACE_ID } from '../ir/namespace';
|
|
5
|
+
export type { Storage } from '../ir/storage';
|
|
6
|
+
export type { StorageType } from '../ir/storage-type';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-level IR alphabet.
|
|
3
|
+
*
|
|
4
|
+
* The framework's contribution to Contract IR / Schema IR is a common
|
|
5
|
+
* root for the IR class hierarchy and a freeze affordance. Family
|
|
6
|
+
* abstract bases (e.g. `SqlNode`, `MongoSchemaIRNode`) refine the alphabet
|
|
7
|
+
* for their family shape; targets ship the concrete classes.
|
|
8
|
+
*
|
|
9
|
+
* `kind` is an optional discriminator on the base. Families and leaves
|
|
10
|
+
* that benefit from discriminated-union dispatch declare their own
|
|
11
|
+
* literal `kind` at the level that earns it — Mongo leaves carry
|
|
12
|
+
* per-class literals (`readonly kind = 'mongo-collection' as const`)
|
|
13
|
+
* because Mongo IR has polymorphic walkers; SQL declares a single
|
|
14
|
+
* family-level `kind = 'sql'` on `SqlNode` because SQL IR has no
|
|
15
|
+
* polymorphic dispatch today. No framework consumer dispatches on
|
|
16
|
+
* `IRNode.kind` at the BASE type — every dispatch site narrows
|
|
17
|
+
* through a union of leaves where each leaf carries a literal kind, so
|
|
18
|
+
* requiring `kind` at the base would be unearned. Future leaves that
|
|
19
|
+
* earn polymorphic dispatch override with a required literal at that
|
|
20
|
+
* leaf (e.g. `override readonly kind = 'postgres-enum' as const`).
|
|
21
|
+
*
|
|
22
|
+
* `IRNodeBase` carries no methods: the freeze-and-assign affordance
|
|
23
|
+
* lives in the free `freezeNode` helper below. Keeping `freezeNode` out
|
|
24
|
+
* of the class type means an emitted contract literal type
|
|
25
|
+
* (`{ readonly kind: 'mongo-collection', ... }` or an unkeyed literal
|
|
26
|
+
* like `{ nativeType, codecId, nullable }`) is structurally assignable
|
|
27
|
+
* to its class type — a `protected freeze()` instance method would
|
|
28
|
+
* otherwise leak into the public type surface and require the literal
|
|
29
|
+
* to carry it too.
|
|
30
|
+
*
|
|
31
|
+
* Subclasses construct fields then call `freezeNode(this)` to seal the
|
|
32
|
+
* instance. Frozen instances + plain readonly fields keep IR nodes
|
|
33
|
+
* JSON-clean by construction, so `JSON.stringify(node)` produces canonical
|
|
34
|
+
* JSON without a `toJSON()` method. The `ContractSerializer` SPI handles
|
|
35
|
+
* round-trip from canonical JSON back to typed class instances.
|
|
36
|
+
*
|
|
37
|
+
* The name (`IRNode` / `IRNodeBase`) reflects the dual-hierarchy reality:
|
|
38
|
+
* this base is the common root for both Contract IR and Schema IR class
|
|
39
|
+
* hierarchies, not a Schema-IR-specific alphabet.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export interface IRNode {
|
|
43
|
+
readonly kind?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export abstract class IRNodeBase implements IRNode {
|
|
47
|
+
abstract readonly kind?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Seal an IR class instance after its constructor has assigned all
|
|
52
|
+
* fields. The free-helper form (rather than a `protected freeze()`
|
|
53
|
+
* instance method) keeps the class type structurally narrow so emitted
|
|
54
|
+
* contract literal types remain assignable to their class types.
|
|
55
|
+
*
|
|
56
|
+
* The helper name stays `freezeNode` — it operates on IR nodes
|
|
57
|
+
* regardless of root naming.
|
|
58
|
+
*/
|
|
59
|
+
export function freezeNode<T extends IRNode>(node: T): T {
|
|
60
|
+
Object.freeze(node);
|
|
61
|
+
return node;
|
|
62
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type IRNode, IRNodeBase } from './ir-node';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reserved sentinel namespace id meaning
|
|
5
|
+
* "no namespace bound at authoring time; resolve from connection context".
|
|
6
|
+
*
|
|
7
|
+
* Materialised target-side as a singleton subclass of the target's
|
|
8
|
+
* `NamespaceBase` concretion that overrides the namespace's
|
|
9
|
+
* qualifier-emission methods to elide the prefix entirely. Call sites
|
|
10
|
+
* stay polymorphic and never branch on `id === UNSPECIFIED_NAMESPACE_ID`
|
|
11
|
+
* — the singleton's overrides drop the qualifier so emitted SQL / Mongo
|
|
12
|
+
* commands look unqualified.
|
|
13
|
+
*
|
|
14
|
+
* Encoded as an exported const (rather than scattered string literals)
|
|
15
|
+
* so the sentinel-id invariant is single-sourced: any production-source
|
|
16
|
+
* site that constructs an unspecified-namespace singleton imports this
|
|
17
|
+
* constant.
|
|
18
|
+
*/
|
|
19
|
+
export const UNSPECIFIED_NAMESPACE_ID = '__unspecified__' as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Framework-level building block for a "namespace" — the database-level
|
|
23
|
+
* grouping under which storage objects (tables, collections, enums, …)
|
|
24
|
+
* reside. Each target's namespace concretion maps the framework concept to
|
|
25
|
+
* a target-native binding:
|
|
26
|
+
*
|
|
27
|
+
* - Postgres: a schema (`CREATE SCHEMA …`); rendered as `"<schema>"`.
|
|
28
|
+
* - SQLite: the singleton `UNSPECIFIED_NAMESPACE_ID`; emitted SQL has no qualifier.
|
|
29
|
+
* - Mongo: the connection's `db` field; addressed as a database name.
|
|
30
|
+
*
|
|
31
|
+
* See `UNSPECIFIED_NAMESPACE_ID` above for the sentinel id and the
|
|
32
|
+
* singleton-subclass pattern that materialises it.
|
|
33
|
+
*/
|
|
34
|
+
export interface Namespace extends IRNode {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export abstract class NamespaceBase extends IRNodeBase implements Namespace {
|
|
39
|
+
abstract readonly id: string;
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IRNode } from './ir-node';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Framework-level alphabet for entries in a storage `types` slot.
|
|
5
|
+
*
|
|
6
|
+
* The slot is polymorphic at the framework level: a family or target can
|
|
7
|
+
* persist either a JSON-clean codec-triple object literal (carrying
|
|
8
|
+
* `kind: 'codec-instance'`) or a class-instance IR node with a narrower
|
|
9
|
+
* kind discriminator (e.g. `'postgres-enum'`). Hydration walkers,
|
|
10
|
+
* verifiers, and planners dispatch on the `kind` literal to recover the
|
|
11
|
+
* precise variant.
|
|
12
|
+
*
|
|
13
|
+
* The `kind` field is required at this layer (in contrast with
|
|
14
|
+
* `IRNode.kind` which is optional) because the slot's downstream
|
|
15
|
+
* consumers dispatch on it — without a guaranteed discriminator the
|
|
16
|
+
* polymorphic walk cannot pick the right reader. Concrete variants
|
|
17
|
+
* narrow `kind` to their literal and add their own field set;
|
|
18
|
+
* downstream consumers use `kind`-discriminator helpers to recover
|
|
19
|
+
* the precise shape.
|
|
20
|
+
*/
|
|
21
|
+
export interface StorageType extends IRNode {
|
|
22
|
+
readonly kind: string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IRNode } from './ir-node';
|
|
2
|
+
import type { Namespace } from './namespace';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Framework-level promise that every Contract IR / Schema IR carries a
|
|
6
|
+
* collection of namespaces keyed by namespace id. Family storage
|
|
7
|
+
* concretions (`SqlStorage`, `MongoStorage`) refine the shape with
|
|
8
|
+
* family-specific fields (tables, collections, enums, …); target
|
|
9
|
+
* concretions add target fields where the family vocabulary doesn't
|
|
10
|
+
* reach.
|
|
11
|
+
*
|
|
12
|
+
* Keeping `namespaces` at the framework layer enforces that every storage
|
|
13
|
+
* object — across any target — is namespace-scoped. The framework can
|
|
14
|
+
* therefore walk the namespace map without knowing the family alphabet, and
|
|
15
|
+
* the `(namespace.id, name)` keying that the verifier and planner depend on
|
|
16
|
+
* is honest at every layer.
|
|
17
|
+
*
|
|
18
|
+
* Extends `IRNode` so the framework's IR-walking surfaces (verifiers,
|
|
19
|
+
* serializers) can dispatch on `Storage`-typed slots through the same
|
|
20
|
+
* IR-node alphabet as every other node — the structural dual already
|
|
21
|
+
* holds in code (every concrete storage class extends an IR-node base);
|
|
22
|
+
* the interface promotion makes the typing honest.
|
|
23
|
+
*
|
|
24
|
+
* **Persisted envelope shape is target-owned, not framework-promised.**
|
|
25
|
+
* Whether the `namespaces` map appears in the on-disk JSON envelope is
|
|
26
|
+
* a per-target decision made by `ContractSerializer.serializeContract`.
|
|
27
|
+
* Some targets emit a JSON-clean namespace shape that round-trips
|
|
28
|
+
* through `JSON.stringify` cleanly (SQL today via the family-layer
|
|
29
|
+
* identity serializer); others ship runtime-only fields on their
|
|
30
|
+
* namespace concretions and override `serializeContract` to strip
|
|
31
|
+
* them (Mongo). Future open (F16): extend the per-target
|
|
32
|
+
* `ContractSerializer` integration-test surface with an explicit
|
|
33
|
+
* envelope-shape assertion for each target, so the strip-vs-pass-through
|
|
34
|
+
* choice is locked at test time rather than implied by the override
|
|
35
|
+
* presence/absence. Earned by PR2's per-target namespace lift, when
|
|
36
|
+
* `PostgresSchema` / `SqliteUnspecifiedDatabase` start carrying
|
|
37
|
+
* target-specific fields.
|
|
38
|
+
*/
|
|
39
|
+
export interface Storage extends IRNode {
|
|
40
|
+
readonly namespaces: Readonly<Record<string, Namespace>>;
|
|
41
|
+
}
|