@prisma-next/framework-components 0.13.0-dev.34 → 0.13.0-dev.36

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/ir.d.mts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { ApplicationDomain, StorageBase, StorageNamespace } from "@prisma-next/contract/types";
2
+ import { isPlainRecord } from "@prisma-next/contract/is-plain-record";
3
+ import { Type } from "arktype";
2
4
 
3
5
  //#region src/ir/ir-node.d.ts
4
6
  /**
@@ -216,6 +218,27 @@ interface Storage extends IRNode {
216
218
  */
217
219
  declare function domainElementCoordinates(domain: Pick<ApplicationDomain, 'namespaces'>): Generator<EntityCoordinate>;
218
220
  //#endregion
221
+ //#region src/ir/entity-kind.d.ts
222
+ interface EntityKindDescriptor<Input, Node> {
223
+ readonly kind: string;
224
+ readonly schema: Type<unknown>;
225
+ readonly construct: (input: Input) => Node;
226
+ }
227
+ type AnyEntityKindDescriptor = EntityKindDescriptor<never, unknown>;
228
+ /**
229
+ * Hydrates a namespace's entities from raw JSON maps into IR class instances.
230
+ *
231
+ * For each kind in `entries`: if the descriptor map has a descriptor,
232
+ * construct each inner-map value; otherwise freeze-and-carry (`'carry'`)
233
+ * or throw naming the kind and nsId (`'fail'`).
234
+ *
235
+ * The single boundary cast hands `value` to `descriptor.construct` as its
236
+ * `Input`. The value satisfies the kind's `Input` either by the
237
+ * entries-input contract at authoring time or by prior `validateStorage`
238
+ * validation at hydration time.
239
+ */
240
+ declare function hydrateNamespaceEntities(entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>, kinds: ReadonlyMap<string, AnyEntityKindDescriptor>, onUnknown: 'carry' | 'fail', nsId?: string): Record<string, Readonly<Record<string, unknown>>>;
241
+ //#endregion
219
242
  //#region src/ir/storage-type.d.ts
220
243
  /**
221
244
  * Framework-level alphabet for entries in a storage `types` slot.
@@ -239,5 +262,5 @@ interface StorageType extends IRNode {
239
262
  readonly kind: string;
240
263
  }
241
264
  //#endregion
242
- export { type EntityCoordinate, type IRNode, IRNodeBase, type Namespace, NamespaceBase, type Storage, type StorageType, UNBOUND_NAMESPACE_ID, domainElementCoordinates, elementCoordinates, entityAt, freezeNode };
265
+ export { type AnyEntityKindDescriptor, type EntityCoordinate, type EntityKindDescriptor, type IRNode, IRNodeBase, type Namespace, NamespaceBase, type Storage, type StorageType, UNBOUND_NAMESPACE_ID, domainElementCoordinates, elementCoordinates, entityAt, freezeNode, hydrateNamespaceEntities, isPlainRecord };
243
266
  //# sourceMappingURL=ir.d.mts.map
package/dist/ir.d.mts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ir.d.mts","names":[],"sources":["../src/ir/ir-node.ts","../src/ir/namespace.ts","../src/ir/storage.ts","../src/ir/domain.ts","../src/ir/storage-type.ts"],"mappings":";;;;;;AAyCA;;;;AACe;AAGf;;;;AACwB;AAYxB;;;;;;;;;;;;;;AAAwD;;;;AChCxD;;;;AAA0D;AAkC1D;;;;UDnBiB,MAAA;EAAA,SACN,IAAI;AAAA;AAAA,uBAGO,UAAA,YAAsB,MAAM;EAAA,kBAC9B,IAAI;AAAA;;;;;;;;;;iBAYR,UAAA,WAAqB,MAAA,EAAQ,IAAA,EAAM,CAAA,GAAI,CAAA;;;;AAjBvD;;;;AACe;AAGf;;;;AACwB;AAYxB;;;;;;;;;;;;cChCa,oBAAA;;ADgC2C;;;;AChCxD;;;;AAA0D;AAkC1D;;;;;;;;;;;;;;;;;;;;AAE2D;AAG3D;UALiB,SAAA,SAAkB,MAAA,EAAQ,gBAAA;EAAA,SAChC,IAAA;EAAA,SACA,OAAA,EAAS,QAAA,CAAS,MAAA,SAAe,QAAA,CAAS,MAAA;AAAA;AAAA,uBAG/B,aAAA,SAAsB,UAAA,YAAsB,SAAA;EAAA,kBAC9C,EAAA;EAAA,kBACA,OAAA,EAAS,QAAA,CAAS,MAAA,SAAe,QAAA,CAAS,MAAA;EAAA,kBACjC,IAAA;AAAA;;;AD3B7B;;;;AACe;AAGf;;;;AACwB;AAYxB;;;;;;AAjBA,UEpBiB,gBAAA;EAAA,SACN,KAAA;EAAA,SACA,WAAA;EAAA,SACA,UAAA;EAAA,SACA,UAAA;AAAA;;;AFiC6C;;;;AChCxD;;;;AAA0D;iBCazC,kBAAA,CACf,OAAA,EAAS,IAAA,CAAK,WAAA,kBACb,SAAA,CAAU,gBAAA;;;;;;;iBAuBG,QAAA,cACd,OAAA,EAAS,IAAA,CAAK,WAAA,iBACd,KAAA,EAAO,IAAA,CAAK,gBAAA,iDACX,CAAA;;;;;;;;;;;;;ADLwD;AAG3D;;;;;;;;;;;;;;;;;;;;;AAGiC;UC+ChB,OAAA,SAAgB,MAAA;EAAA,SACtB,UAAA,EAAY,QAAA,CAAS,MAAA,SAAe,SAAA;AAAA;;;;AF3E/C;;;;AACe;AAGf;iBGnCiB,wBAAA,CACf,MAAA,EAAQ,IAAA,CAAK,iBAAA,kBACZ,SAAA,CAAU,gBAAA;;;;;AH6Bb;;;;AACe;AAGf;;;;AACwB;AAYxB;;;;;;UItCiB,WAAA,SAAoB,MAAM;EAAA,SAChC,IAAI;AAAA"}
1
+ {"version":3,"file":"ir.d.mts","names":[],"sources":["../src/ir/ir-node.ts","../src/ir/namespace.ts","../src/ir/storage.ts","../src/ir/domain.ts","../src/ir/entity-kind.ts","../src/ir/storage-type.ts"],"mappings":";;;;;;;;;;AAyCA;;;;AACe;AAGf;;;;AACwB;AAYxB;;;;;;;;;;;;;;AAAwD;;;;AChCxD;;;;AAA0D;AAkC1D;;UDnBiB,MAAA;EAAA,SACN,IAAI;AAAA;AAAA,uBAGO,UAAA,YAAsB,MAAM;EAAA,kBAC9B,IAAI;AAAA;;;;;;;;;;iBAYR,UAAA,WAAqB,MAAA,EAAQ,IAAA,EAAM,CAAA,GAAI,CAAA;;;;;;AAjBvD;;;;AACe;AAGf;;;;AACwB;AAYxB;;;;;;;;;;cChCa,oBAAA;;;;ADgC2C;;;;AChCxD;;;;AAA0D;AAkC1D;;;;;;;;;;;;;;;;;;;;UAAiB,SAAA,SAAkB,MAAA,EAAQ,gBAAA;EAAA,SAChC,IAAA;EAAA,SACA,OAAA,EAAS,QAAA,CAAS,MAAA,SAAe,QAAA,CAAS,MAAA;AAAA;AAAA,uBAG/B,aAAA,SAAsB,UAAA,YAAsB,SAAA;EAAA,kBAC9C,EAAA;EAAA,kBACA,OAAA,EAAS,QAAA,CAAS,MAAA,SAAe,QAAA,CAAS,MAAA;EAAA,kBACjC,IAAA;AAAA;;;AD3B7B;;;;AACe;AAGf;;;;AACwB;AAYxB;;;;;;AAjBA,UEjBiB,gBAAA;EAAA,SACN,KAAA;EAAA,SACA,WAAA;EAAA,SACA,UAAA;EAAA,SACA,UAAA;AAAA;;;AF8B6C;;;;AChCxD;;;;AAA0D;iBCgBzC,kBAAA,CACf,OAAA,EAAS,IAAA,CAAK,WAAA,kBACb,SAAA,CAAU,gBAAA;;;;;;;iBAmBG,QAAA,cACd,OAAA,EAAS,IAAA,CAAK,WAAA,iBACd,KAAA,EAAO,IAAA,CAAK,gBAAA,iDACX,CAAA;;;;;;;;;;;;;ADJwD;AAG3D;;;;;;;;;;;;;;;;;;;;;AAGiC;UC8ChB,OAAA,SAAgB,MAAA;EAAA,SACtB,UAAA,EAAY,QAAA,CAAS,MAAA,SAAe,SAAA;AAAA;;;;;;AF1E/C;;;;iBG/BiB,wBAAA,CACf,MAAA,EAAQ,IAAA,CAAK,iBAAA,kBACZ,SAAA,CAAU,gBAAA;;;UCTI,oBAAA;EAAA,SACN,IAAA;EAAA,SAEA,MAAA,EAAQ,IAAA;EAAA,SACR,SAAA,GAAY,KAAA,EAAO,KAAA,KAAU,IAAA;AAAA;AAAA,KAG5B,uBAAA,GAA0B,oBAAoB;;;AJgC3C;AAGf;;;;AACwB;AAYxB;;;;iBIlCgB,wBAAA,CACd,OAAA,EAAS,QAAA,CAAS,MAAA,SAAe,QAAA,CAAS,MAAA,sBAC1C,KAAA,EAAO,WAAA,SAAoB,uBAAA,GAC3B,SAAA,oBACA,IAAA,YACC,MAAA,SAAe,QAAA,CAAS,MAAA;;;;;;;AJY3B;;;;AACe;AAGf;;;;AACwB;AAYxB;;;;UKtCiB,WAAA,SAAoB,MAAM;EAAA,SAChC,IAAI;AAAA"}
package/dist/ir.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { blindCast } from "@prisma-next/utils/casts";
2
+ import { isPlainRecord } from "@prisma-next/contract/is-plain-record";
2
3
  //#region src/ir/domain.ts
3
4
  /**
4
5
  * Lazy walk over every named domain entity in a {@link ApplicationDomain},
@@ -20,6 +21,33 @@ function* domainElementCoordinates(domain) {
20
21
  }
21
22
  }
22
23
  //#endregion
24
+ //#region src/ir/entity-kind.ts
25
+ /**
26
+ * Hydrates a namespace's entities from raw JSON maps into IR class instances.
27
+ *
28
+ * For each kind in `entries`: if the descriptor map has a descriptor,
29
+ * construct each inner-map value; otherwise freeze-and-carry (`'carry'`)
30
+ * or throw naming the kind and nsId (`'fail'`).
31
+ *
32
+ * The single boundary cast hands `value` to `descriptor.construct` as its
33
+ * `Input`. The value satisfies the kind's `Input` either by the
34
+ * entries-input contract at authoring time or by prior `validateStorage`
35
+ * validation at hydration time.
36
+ */
37
+ function hydrateNamespaceEntities(entries, kinds, onUnknown, nsId) {
38
+ const result = {};
39
+ for (const [kind, rawMap] of Object.entries(entries)) {
40
+ const descriptor = kinds.get(kind);
41
+ if (descriptor !== void 0) {
42
+ const built = {};
43
+ for (const [name, value] of Object.entries(rawMap)) built[name] = descriptor.construct(blindCast(value));
44
+ result[kind] = Object.freeze(built);
45
+ } else if (onUnknown === "carry") result[kind] = Object.freeze(rawMap);
46
+ else throw new Error(`Unknown entries key "${kind}" in namespace "${nsId ?? "?"}"; no hydration factory registered for this entity kind`);
47
+ }
48
+ return result;
49
+ }
50
+ //#endregion
23
51
  //#region src/ir/ir-node.ts
24
52
  var IRNodeBase = class {};
25
53
  /**
@@ -90,9 +118,6 @@ function* elementCoordinates(storage) {
90
118
  }
91
119
  }
92
120
  }
93
- function isRecord(value) {
94
- return typeof value === "object" && value !== null;
95
- }
96
121
  /**
97
122
  * Looks up a single entity in a `Storage`-shaped value by its full coordinate.
98
123
  * Returns `undefined` if the namespace, entity kind, or entity name is absent.
@@ -103,13 +128,13 @@ function entityAt(storage, coord) {
103
128
  const ns = storage.namespaces[coord.namespaceId];
104
129
  if (ns === void 0) return void 0;
105
130
  const entries = ns.entries;
106
- if (!isRecord(entries)) return void 0;
131
+ if (!isPlainRecord(entries)) return void 0;
107
132
  const kindMap = entries[coord.entityKind];
108
- if (!isRecord(kindMap)) return void 0;
133
+ if (!isPlainRecord(kindMap)) return void 0;
109
134
  if (!Object.hasOwn(kindMap, coord.entityName)) return void 0;
110
135
  return blindCast(kindMap[coord.entityName]);
111
136
  }
112
137
  //#endregion
113
- export { IRNodeBase, NamespaceBase, UNBOUND_NAMESPACE_ID, domainElementCoordinates, elementCoordinates, entityAt, freezeNode };
138
+ export { IRNodeBase, NamespaceBase, UNBOUND_NAMESPACE_ID, domainElementCoordinates, elementCoordinates, entityAt, freezeNode, hydrateNamespaceEntities, isPlainRecord };
114
139
 
115
140
  //# sourceMappingURL=ir.mjs.map
package/dist/ir.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ir.mjs","names":[],"sources":["../src/ir/domain.ts","../src/ir/ir-node.ts","../src/ir/namespace.ts","../src/ir/storage.ts"],"sourcesContent":["import type { ApplicationDomain } from '@prisma-next/contract/types';\nimport type { EntityCoordinate } from './storage';\n\n/**\n * Lazy walk over every named domain entity in a {@link ApplicationDomain},\n * yielded as {@link EntityCoordinate} tuples with `plane: 'domain'`.\n *\n * Same structural rules as {@link elementCoordinates} over storage: skip\n * scalar `id`; each other object-valued property is an entity-kind slot.\n */\nexport function* domainElementCoordinates(\n domain: Pick<ApplicationDomain, 'namespaces'>,\n): Generator<EntityCoordinate> {\n for (const [namespaceId, ns] of Object.entries(domain.namespaces)) {\n for (const [entityKind, slot] of Object.entries(ns)) {\n if (entityKind === 'id') continue;\n if (slot === null || typeof slot !== 'object') continue;\n for (const entityName of Object.keys(slot)) {\n yield { plane: 'domain', namespaceId, entityKind, entityName };\n }\n }\n }\n}\n","/**\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 = 'pack-contributed-kind' 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 { StorageNamespace } from '@prisma-next/contract/types';\nimport { type IRNode, IRNodeBase } from './ir-node';\n\n/**\n * Reserved sentinel namespace id for the late-bound storage slot —\n * the slot whose binding the target resolves at connection time\n * rather than at authoring time. Postgres uses it for `search_path`\n * late binding; SQLite uses it for the trivial singleton; Mongo uses\n * it for the connection's `db` binding.\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 === UNBOUND_NAMESPACE_ID`\n * — the singleton's overrides drop the qualifier so emitted SQL / Mongo\n * commands look unqualified.\n *\n * The double-underscore decoration marks the id as a framework-reserved\n * coordinate when it appears in a JSON envelope (cold-read-as-reserved\n * — no realistic collision with user-declared namespace names).\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 unbound-namespace singleton imports this\n * constant.\n */\nexport const UNBOUND_NAMESPACE_ID = '__unbound__' 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 `UNBOUND_NAMESPACE_ID`; emitted SQL has no qualifier.\n * - Mongo: the connection's `db` field; addressed as a database name.\n *\n * See `UNBOUND_NAMESPACE_ID` above for the sentinel id and the\n * singleton-subclass pattern that materialises it.\n *\n * The framework promises only the coordinate (`id`) — the named storage\n * entities a namespace contains are family-typed (SQL contributes\n * `table` / `type`, Mongo contributes `collection`, future families pick\n * their own native idiom under `entries`). Generic consumers walking \"all\n * named entries\" go through a family-typed namespace, not the framework\n * `Namespace`.\n *\n * Every namespace concretion (e.g. family-built SQL namespaces,\n * `MongoUnboundNamespace`, target-promoted namespaces like\n * `PostgresSchema`) carries exactly: `id` (enumerable string),\n * `entries` (frozen object holding entity-kind slot maps), and `kind`\n * (non-enumerable string discriminator set via `Object.defineProperty`).\n * Each slot map under `entries` uses a singular essence key (`table`,\n * `type`, `collection`, …) mapping entity names to IR classes. No other\n * own-enumerable data lives on a namespace; non-entity computed data lives\n * on the surrounding storage or contract IR. The framework's\n * `elementCoordinates(storage)` walk relies on this invariant to enumerate\n * entities structurally without family-specific knowledge.\n */\nexport interface Namespace extends IRNode, StorageNamespace {\n readonly kind: string;\n readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;\n}\n\nexport abstract class NamespaceBase extends IRNodeBase implements Namespace {\n abstract readonly id: string;\n abstract readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;\n abstract override readonly kind: string;\n}\n","import type { StorageBase } from '@prisma-next/contract/types';\nimport { blindCast } from '@prisma-next/utils/casts';\nimport type { IRNode } from './ir-node';\nimport type { Namespace } from './namespace';\n\n/**\n * Canonical address for a named entity in Contract IR / Schema IR.\n *\n * `plane` is `'domain' | 'storage'`: which top-level contract plane the\n * entity lives on. Domain-side walks yield `plane: 'domain'` via\n * {@link domainElementCoordinates}; {@link elementCoordinates} over storage\n * yields `plane: 'storage'`.\n *\n * Cross-plane references obey a directional invariant: domain → storage is\n * allowed; storage → domain is forbidden. That rule is enforced by a\n * separate validator, not by constraining this coordinate shape — the\n * coordinate carries the axis the validator checks.\n *\n * Iteration order over namespace properties follows `Object.entries` order;\n * consumers that depend on ordering must sort.\n */\nexport interface EntityCoordinate {\n readonly plane: 'domain' | 'storage';\n readonly namespaceId: string;\n readonly entityKind: string;\n readonly entityName: string;\n}\n\n/**\n * Lazy walk over every named storage entity in a `Storage`-shaped\n * value, yielded as {@link EntityCoordinate} tuples with\n * `plane: 'storage'` (the parameter type binds the plane).\n *\n * Iterates each namespace's `entries` kind maps structurally. Skips\n * non-object `entries`; `id` and `kind` are not walked (`kind` is\n * non-enumerable on concretions). For every entity-kind key under\n * `entries` whose value is a non-null object, yields one coordinate per\n * entity name in that map. No family-specific kind vocabulary is required.\n */\nexport function* elementCoordinates(\n storage: Pick<StorageBase, 'namespaces'>,\n): Generator<EntityCoordinate> {\n for (const [namespaceId, ns] of Object.entries(storage.namespaces)) {\n const entries = ns.entries;\n if (entries === null || typeof entries !== 'object') continue;\n for (const [entityKind, kindMap] of Object.entries(entries)) {\n if (kindMap === null || typeof kindMap !== 'object') continue;\n for (const entityName of Object.keys(kindMap)) {\n yield { plane: 'storage', namespaceId, entityKind, entityName };\n }\n }\n }\n}\n\nfunction isRecord(value: unknown): value is Readonly<Record<string, unknown>> {\n return typeof value === 'object' && value !== null;\n}\n\n/**\n * Looks up a single entity in a `Storage`-shaped value by its full coordinate.\n * Returns `undefined` if the namespace, entity kind, or entity name is absent.\n * The type parameter is a caller assertion — the walk itself is structural\n * and cannot verify the entity's shape.\n */\nexport function entityAt<T = unknown>(\n storage: Pick<StorageBase, 'namespaces'>,\n coord: Pick<EntityCoordinate, 'namespaceId' | 'entityKind' | 'entityName'>,\n): T | undefined {\n const ns = storage.namespaces[coord.namespaceId];\n if (ns === undefined) return undefined;\n const entries = ns.entries;\n if (!isRecord(entries)) return undefined;\n const kindMap = entries[coord.entityKind];\n if (!isRecord(kindMap)) return undefined;\n if (!Object.hasOwn(kindMap, coord.entityName)) return undefined;\n return blindCast<T | undefined, 'caller asserts the entity type at this coordinate'>(\n kindMap[coord.entityName],\n );\n}\n\n/**\n * Framework-level promise that every Contract IR / Schema IR carries a\n * collection of namespaces keyed by namespace id. Family storage\n * concretions (`SqlStorage`, `MongoStorage`) refine the shape with\n * family-specific fields (tables, collections, enums, …); target\n * concretions add target fields where the family vocabulary doesn't\n * reach.\n *\n * Keeping `namespaces` at the framework layer enforces that every storage\n * object — across any target — is namespace-scoped. The framework can\n * therefore walk the namespace map without knowing the family alphabet, and\n * the `(namespace.id, name)` keying that the verifier and planner depend on\n * is honest at every layer.\n *\n * Extends `IRNode` so the framework's IR-walking surfaces (verifiers,\n * serializers) can dispatch on `Storage`-typed fields through the same\n * IR-node alphabet as every other node — the structural dual already\n * holds in code (every concrete storage class extends an IR-node base);\n * the interface promotion makes the typing honest.\n *\n * **Persisted envelope shape is target-owned, not framework-promised.**\n * Whether the `namespaces` map appears in the on-disk JSON envelope is\n * a per-target decision made by `ContractSerializer.serializeContract`.\n * Some targets emit a JSON-clean namespace shape that round-trips\n * through `JSON.stringify` cleanly (SQL today via the family-layer\n * identity serializer); others ship runtime-only fields on their\n * namespace concretions and override `serializeContract` to strip\n * them (Mongo). Future open (F16): extend the per-target\n * `ContractSerializer` integration-test surface with an explicit\n * envelope-shape assertion for each target, so the strip-vs-pass-through\n * choice is locked at test time rather than implied by the override\n * presence/absence. Earned by PR2's per-target namespace lift, when\n * `PostgresSchema` / `SqliteUnboundDatabase` start carrying\n * target-specific fields.\n */\nexport interface Storage extends IRNode {\n readonly namespaces: Readonly<Record<string, Namespace>>;\n}\n"],"mappings":";;;;;;;;;AAUA,UAAiB,yBACf,QAC6B;CAC7B,KAAK,MAAM,CAAC,aAAa,OAAO,OAAO,QAAQ,OAAO,UAAU,GAC9D,KAAK,MAAM,CAAC,YAAY,SAAS,OAAO,QAAQ,EAAE,GAAG;EACnD,IAAI,eAAe,MAAM;EACzB,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU;EAC/C,KAAK,MAAM,cAAc,OAAO,KAAK,IAAI,GACvC,MAAM;GAAE,OAAO;GAAU;GAAa;GAAY;EAAW;CAEjE;AAEJ;;;ACuBA,IAAsB,aAAtB,MAAmD,CAEnD;;;;;;;;;;AAWA,SAAgB,WAA6B,MAAY;CACvD,OAAO,OAAO,IAAI;CAClB,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;ACnCA,MAAa,uBAAuB;AAuCpC,IAAsB,gBAAtB,cAA4C,WAAgC,CAI5E;;;;;;;;;;;;;;AC9BA,UAAiB,mBACf,SAC6B;CAC7B,KAAK,MAAM,CAAC,aAAa,OAAO,OAAO,QAAQ,QAAQ,UAAU,GAAG;EAClE,MAAM,UAAU,GAAG;EACnB,IAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;EACrD,KAAK,MAAM,CAAC,YAAY,YAAY,OAAO,QAAQ,OAAO,GAAG;GAC3D,IAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;GACrD,KAAK,MAAM,cAAc,OAAO,KAAK,OAAO,GAC1C,MAAM;IAAE,OAAO;IAAW;IAAa;IAAY;GAAW;EAElE;CACF;AACF;AAEA,SAAS,SAAS,OAA4D;CAC5E,OAAO,OAAO,UAAU,YAAY,UAAU;AAChD;;;;;;;AAQA,SAAgB,SACd,SACA,OACe;CACf,MAAM,KAAK,QAAQ,WAAW,MAAM;CACpC,IAAI,OAAO,KAAA,GAAW,OAAO,KAAA;CAC7B,MAAM,UAAU,GAAG;CACnB,IAAI,CAAC,SAAS,OAAO,GAAG,OAAO,KAAA;CAC/B,MAAM,UAAU,QAAQ,MAAM;CAC9B,IAAI,CAAC,SAAS,OAAO,GAAG,OAAO,KAAA;CAC/B,IAAI,CAAC,OAAO,OAAO,SAAS,MAAM,UAAU,GAAG,OAAO,KAAA;CACtD,OAAO,UACL,QAAQ,MAAM,WAChB;AACF"}
1
+ {"version":3,"file":"ir.mjs","names":[],"sources":["../src/ir/domain.ts","../src/ir/entity-kind.ts","../src/ir/ir-node.ts","../src/ir/namespace.ts","../src/ir/storage.ts"],"sourcesContent":["import type { ApplicationDomain } from '@prisma-next/contract/types';\nimport type { EntityCoordinate } from './storage';\n\n/**\n * Lazy walk over every named domain entity in a {@link ApplicationDomain},\n * yielded as {@link EntityCoordinate} tuples with `plane: 'domain'`.\n *\n * Same structural rules as {@link elementCoordinates} over storage: skip\n * scalar `id`; each other object-valued property is an entity-kind slot.\n */\nexport function* domainElementCoordinates(\n domain: Pick<ApplicationDomain, 'namespaces'>,\n): Generator<EntityCoordinate> {\n for (const [namespaceId, ns] of Object.entries(domain.namespaces)) {\n for (const [entityKind, slot] of Object.entries(ns)) {\n if (entityKind === 'id') continue;\n if (slot === null || typeof slot !== 'object') continue;\n for (const entityName of Object.keys(slot)) {\n yield { plane: 'domain', namespaceId, entityKind, entityName };\n }\n }\n }\n}\n","import { blindCast } from '@prisma-next/utils/casts';\nimport type { Type } from 'arktype';\n\nexport interface EntityKindDescriptor<Input, Node> {\n readonly kind: string;\n // Type<unknown>, not Type<Input>: AnyEntityKindDescriptor widens Input to never, which would force an unusable Type<never>; concrete descriptors still carry their real schema.\n readonly schema: Type<unknown>;\n readonly construct: (input: Input) => Node;\n}\n\nexport type AnyEntityKindDescriptor = EntityKindDescriptor<never, unknown>;\n\n/**\n * Hydrates a namespace's entities from raw JSON maps into IR class instances.\n *\n * For each kind in `entries`: if the descriptor map has a descriptor,\n * construct each inner-map value; otherwise freeze-and-carry (`'carry'`)\n * or throw naming the kind and nsId (`'fail'`).\n *\n * The single boundary cast hands `value` to `descriptor.construct` as its\n * `Input`. The value satisfies the kind's `Input` either by the\n * entries-input contract at authoring time or by prior `validateStorage`\n * validation at hydration time.\n */\nexport function hydrateNamespaceEntities(\n entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>,\n kinds: ReadonlyMap<string, AnyEntityKindDescriptor>,\n onUnknown: 'carry' | 'fail',\n nsId?: string,\n): Record<string, Readonly<Record<string, unknown>>> {\n const result: Record<string, Readonly<Record<string, unknown>>> = {};\n for (const [kind, rawMap] of Object.entries(entries)) {\n const descriptor = kinds.get(kind);\n if (descriptor !== undefined) {\n const built: Record<string, unknown> = {};\n for (const [name, value] of Object.entries(rawMap)) {\n built[name] = descriptor.construct(\n blindCast<\n never,\n \"value is this kind's descriptor Input: when authoring, the typed entries-input contract produces it; when hydrating, it was validated against descriptor.schema before this loop. The never target is AnyEntityKindDescriptor's erased Input parameter.\"\n >(value),\n );\n }\n result[kind] = Object.freeze(built);\n } else if (onUnknown === 'carry') {\n result[kind] = Object.freeze(rawMap);\n } else {\n throw new Error(\n `Unknown entries key \"${kind}\" in namespace \"${nsId ?? '?'}\"; no hydration factory registered for this entity kind`,\n );\n }\n }\n return result;\n}\n","/**\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 = 'pack-contributed-kind' 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 { StorageNamespace } from '@prisma-next/contract/types';\nimport { type IRNode, IRNodeBase } from './ir-node';\n\n/**\n * Reserved sentinel namespace id for the late-bound storage slot —\n * the slot whose binding the target resolves at connection time\n * rather than at authoring time. Postgres uses it for `search_path`\n * late binding; SQLite uses it for the trivial singleton; Mongo uses\n * it for the connection's `db` binding.\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 === UNBOUND_NAMESPACE_ID`\n * — the singleton's overrides drop the qualifier so emitted SQL / Mongo\n * commands look unqualified.\n *\n * The double-underscore decoration marks the id as a framework-reserved\n * coordinate when it appears in a JSON envelope (cold-read-as-reserved\n * — no realistic collision with user-declared namespace names).\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 unbound-namespace singleton imports this\n * constant.\n */\nexport const UNBOUND_NAMESPACE_ID = '__unbound__' 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 `UNBOUND_NAMESPACE_ID`; emitted SQL has no qualifier.\n * - Mongo: the connection's `db` field; addressed as a database name.\n *\n * See `UNBOUND_NAMESPACE_ID` above for the sentinel id and the\n * singleton-subclass pattern that materialises it.\n *\n * The framework promises only the coordinate (`id`) — the named storage\n * entities a namespace contains are family-typed (SQL contributes\n * `table` / `type`, Mongo contributes `collection`, future families pick\n * their own native idiom under `entries`). Generic consumers walking \"all\n * named entries\" go through a family-typed namespace, not the framework\n * `Namespace`.\n *\n * Every namespace concretion (e.g. family-built SQL namespaces,\n * `MongoUnboundNamespace`, target-promoted namespaces like\n * `PostgresSchema`) carries exactly: `id` (enumerable string),\n * `entries` (frozen object holding entity-kind slot maps), and `kind`\n * (non-enumerable string discriminator set via `Object.defineProperty`).\n * Each slot map under `entries` uses a singular essence key (`table`,\n * `type`, `collection`, …) mapping entity names to IR classes. No other\n * own-enumerable data lives on a namespace; non-entity computed data lives\n * on the surrounding storage or contract IR. The framework's\n * `elementCoordinates(storage)` walk relies on this invariant to enumerate\n * entities structurally without family-specific knowledge.\n */\nexport interface Namespace extends IRNode, StorageNamespace {\n readonly kind: string;\n readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;\n}\n\nexport abstract class NamespaceBase extends IRNodeBase implements Namespace {\n abstract readonly id: string;\n abstract readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;\n abstract override readonly kind: string;\n}\n","import { isPlainRecord } from '@prisma-next/contract/is-plain-record';\nimport type { StorageBase } from '@prisma-next/contract/types';\nimport { blindCast } from '@prisma-next/utils/casts';\nimport type { IRNode } from './ir-node';\nimport type { Namespace } from './namespace';\n\nexport { isPlainRecord };\n\n/**\n * Canonical address for a named entity in Contract IR / Schema IR.\n *\n * `plane` is `'domain' | 'storage'`: which top-level contract plane the\n * entity lives on. Domain-side walks yield `plane: 'domain'` via\n * {@link domainElementCoordinates}; {@link elementCoordinates} over storage\n * yields `plane: 'storage'`.\n *\n * Cross-plane references obey a directional invariant: domain → storage is\n * allowed; storage → domain is forbidden. That rule is enforced by a\n * separate validator, not by constraining this coordinate shape — the\n * coordinate carries the axis the validator checks.\n *\n * Iteration order over namespace properties follows `Object.entries` order;\n * consumers that depend on ordering must sort.\n */\nexport interface EntityCoordinate {\n readonly plane: 'domain' | 'storage';\n readonly namespaceId: string;\n readonly entityKind: string;\n readonly entityName: string;\n}\n\n/**\n * Lazy walk over every named storage entity in a `Storage`-shaped\n * value, yielded as {@link EntityCoordinate} tuples with\n * `plane: 'storage'` (the parameter type binds the plane).\n *\n * Iterates each namespace's `entries` kind maps structurally. Skips\n * non-object `entries`; `id` and `kind` are not walked (`kind` is\n * non-enumerable on concretions). For every entity-kind key under\n * `entries` whose value is a non-null object, yields one coordinate per\n * entity name in that map. No family-specific kind vocabulary is required.\n */\nexport function* elementCoordinates(\n storage: Pick<StorageBase, 'namespaces'>,\n): Generator<EntityCoordinate> {\n for (const [namespaceId, ns] of Object.entries(storage.namespaces)) {\n const entries = ns.entries;\n if (entries === null || typeof entries !== 'object') continue;\n for (const [entityKind, kindMap] of Object.entries(entries)) {\n if (kindMap === null || typeof kindMap !== 'object') continue;\n for (const entityName of Object.keys(kindMap)) {\n yield { plane: 'storage', namespaceId, entityKind, entityName };\n }\n }\n }\n}\n\n/**\n * Looks up a single entity in a `Storage`-shaped value by its full coordinate.\n * Returns `undefined` if the namespace, entity kind, or entity name is absent.\n * The type parameter is a caller assertion — the walk itself is structural\n * and cannot verify the entity's shape.\n */\nexport function entityAt<T = unknown>(\n storage: Pick<StorageBase, 'namespaces'>,\n coord: Pick<EntityCoordinate, 'namespaceId' | 'entityKind' | 'entityName'>,\n): T | undefined {\n const ns = storage.namespaces[coord.namespaceId];\n if (ns === undefined) return undefined;\n const entries = ns.entries;\n if (!isPlainRecord(entries)) return undefined;\n const kindMap = entries[coord.entityKind];\n if (!isPlainRecord(kindMap)) return undefined;\n if (!Object.hasOwn(kindMap, coord.entityName)) return undefined;\n return blindCast<T | undefined, 'caller asserts the entity type at this coordinate'>(\n kindMap[coord.entityName],\n );\n}\n\n/**\n * Framework-level promise that every Contract IR / Schema IR carries a\n * collection of namespaces keyed by namespace id. Family storage\n * concretions (`SqlStorage`, `MongoStorage`) refine the shape with\n * family-specific fields (tables, collections, enums, …); target\n * concretions add target fields where the family vocabulary doesn't\n * reach.\n *\n * Keeping `namespaces` at the framework layer enforces that every storage\n * object — across any target — is namespace-scoped. The framework can\n * therefore walk the namespace map without knowing the family alphabet, and\n * the `(namespace.id, name)` keying that the verifier and planner depend on\n * is honest at every layer.\n *\n * Extends `IRNode` so the framework's IR-walking surfaces (verifiers,\n * serializers) can dispatch on `Storage`-typed fields through the same\n * IR-node alphabet as every other node — the structural dual already\n * holds in code (every concrete storage class extends an IR-node base);\n * the interface promotion makes the typing honest.\n *\n * **Persisted envelope shape is target-owned, not framework-promised.**\n * Whether the `namespaces` map appears in the on-disk JSON envelope is\n * a per-target decision made by `ContractSerializer.serializeContract`.\n * Some targets emit a JSON-clean namespace shape that round-trips\n * through `JSON.stringify` cleanly (SQL today via the family-layer\n * identity serializer); others ship runtime-only fields on their\n * namespace concretions and override `serializeContract` to strip\n * them (Mongo). Future open (F16): extend the per-target\n * `ContractSerializer` integration-test surface with an explicit\n * envelope-shape assertion for each target, so the strip-vs-pass-through\n * choice is locked at test time rather than implied by the override\n * presence/absence. Earned by PR2's per-target namespace lift, when\n * `PostgresSchema` / `SqliteUnboundDatabase` start carrying\n * target-specific fields.\n */\nexport interface Storage extends IRNode {\n readonly namespaces: Readonly<Record<string, Namespace>>;\n}\n"],"mappings":";;;;;;;;;;AAUA,UAAiB,yBACf,QAC6B;CAC7B,KAAK,MAAM,CAAC,aAAa,OAAO,OAAO,QAAQ,OAAO,UAAU,GAC9D,KAAK,MAAM,CAAC,YAAY,SAAS,OAAO,QAAQ,EAAE,GAAG;EACnD,IAAI,eAAe,MAAM;EACzB,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU;EAC/C,KAAK,MAAM,cAAc,OAAO,KAAK,IAAI,GACvC,MAAM;GAAE,OAAO;GAAU;GAAa;GAAY;EAAW;CAEjE;AAEJ;;;;;;;;;;;;;;;ACEA,SAAgB,yBACd,SACA,OACA,WACA,MACmD;CACnD,MAAM,SAA4D,CAAC;CACnE,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,OAAO,GAAG;EACpD,MAAM,aAAa,MAAM,IAAI,IAAI;EACjC,IAAI,eAAe,KAAA,GAAW;GAC5B,MAAM,QAAiC,CAAC;GACxC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,GAC/C,MAAM,QAAQ,WAAW,UACvB,UAGE,KAAK,CACT;GAEF,OAAO,QAAQ,OAAO,OAAO,KAAK;EACpC,OAAO,IAAI,cAAc,SACvB,OAAO,QAAQ,OAAO,OAAO,MAAM;OAEnC,MAAM,IAAI,MACR,wBAAwB,KAAK,kBAAkB,QAAQ,IAAI,wDAC7D;CAEJ;CACA,OAAO;AACT;;;ACRA,IAAsB,aAAtB,MAAmD,CAEnD;;;;;;;;;;AAWA,SAAgB,WAA6B,MAAY;CACvD,OAAO,OAAO,IAAI;CAClB,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;ACnCA,MAAa,uBAAuB;AAuCpC,IAAsB,gBAAtB,cAA4C,WAAgC,CAI5E;;;;;;;;;;;;;;AC3BA,UAAiB,mBACf,SAC6B;CAC7B,KAAK,MAAM,CAAC,aAAa,OAAO,OAAO,QAAQ,QAAQ,UAAU,GAAG;EAClE,MAAM,UAAU,GAAG;EACnB,IAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;EACrD,KAAK,MAAM,CAAC,YAAY,YAAY,OAAO,QAAQ,OAAO,GAAG;GAC3D,IAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;GACrD,KAAK,MAAM,cAAc,OAAO,KAAK,OAAO,GAC1C,MAAM;IAAE,OAAO;IAAW;IAAa;IAAY;GAAW;EAElE;CACF;AACF;;;;;;;AAQA,SAAgB,SACd,SACA,OACe;CACf,MAAM,KAAK,QAAQ,WAAW,MAAM;CACpC,IAAI,OAAO,KAAA,GAAW,OAAO,KAAA;CAC7B,MAAM,UAAU,GAAG;CACnB,IAAI,CAAC,cAAc,OAAO,GAAG,OAAO,KAAA;CACpC,MAAM,UAAU,QAAQ,MAAM;CAC9B,IAAI,CAAC,cAAc,OAAO,GAAG,OAAO,KAAA;CACpC,IAAI,CAAC,OAAO,OAAO,SAAS,MAAM,UAAU,GAAG,OAAO,KAAA;CACtD,OAAO,UACL,QAAQ,MAAM,WAChB;AACF"}
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@prisma-next/framework-components",
3
- "version": "0.13.0-dev.34",
3
+ "version": "0.13.0-dev.36",
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.13.0-dev.34",
10
- "@prisma-next/operations": "0.13.0-dev.34",
11
- "@prisma-next/ts-render": "0.13.0-dev.34",
12
- "@prisma-next/utils": "0.13.0-dev.34",
9
+ "@prisma-next/contract": "0.13.0-dev.36",
10
+ "@prisma-next/operations": "0.13.0-dev.36",
11
+ "@prisma-next/ts-render": "0.13.0-dev.36",
12
+ "@prisma-next/utils": "0.13.0-dev.36",
13
13
  "@standard-schema/spec": "^1.1.0",
14
14
  "arktype": "^2.2.0"
15
15
  },
16
16
  "devDependencies": {
17
- "@prisma-next/tsconfig": "0.13.0-dev.34",
18
- "@prisma-next/tsdown": "0.13.0-dev.34",
17
+ "@prisma-next/tsconfig": "0.13.0-dev.36",
18
+ "@prisma-next/tsdown": "0.13.0-dev.36",
19
19
  "tsdown": "0.22.1",
20
20
  "typescript": "5.9.3",
21
21
  "vitest": "4.1.8"
package/src/exports/ir.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export { domainElementCoordinates } from '../ir/domain';
2
+ export type { AnyEntityKindDescriptor, EntityKindDescriptor } from '../ir/entity-kind';
3
+ export { hydrateNamespaceEntities } from '../ir/entity-kind';
2
4
  export type { IRNode } from '../ir/ir-node';
3
5
  export { freezeNode, IRNodeBase } from '../ir/ir-node';
4
6
  export type { Namespace } from '../ir/namespace';
5
7
  export { NamespaceBase, UNBOUND_NAMESPACE_ID } from '../ir/namespace';
6
8
  export type { EntityCoordinate, Storage } from '../ir/storage';
7
- export { elementCoordinates, entityAt } from '../ir/storage';
9
+ export { elementCoordinates, entityAt, isPlainRecord } from '../ir/storage';
8
10
  export type { StorageType } from '../ir/storage-type';
@@ -0,0 +1,54 @@
1
+ import { blindCast } from '@prisma-next/utils/casts';
2
+ import type { Type } from 'arktype';
3
+
4
+ export interface EntityKindDescriptor<Input, Node> {
5
+ readonly kind: string;
6
+ // Type<unknown>, not Type<Input>: AnyEntityKindDescriptor widens Input to never, which would force an unusable Type<never>; concrete descriptors still carry their real schema.
7
+ readonly schema: Type<unknown>;
8
+ readonly construct: (input: Input) => Node;
9
+ }
10
+
11
+ export type AnyEntityKindDescriptor = EntityKindDescriptor<never, unknown>;
12
+
13
+ /**
14
+ * Hydrates a namespace's entities from raw JSON maps into IR class instances.
15
+ *
16
+ * For each kind in `entries`: if the descriptor map has a descriptor,
17
+ * construct each inner-map value; otherwise freeze-and-carry (`'carry'`)
18
+ * or throw naming the kind and nsId (`'fail'`).
19
+ *
20
+ * The single boundary cast hands `value` to `descriptor.construct` as its
21
+ * `Input`. The value satisfies the kind's `Input` either by the
22
+ * entries-input contract at authoring time or by prior `validateStorage`
23
+ * validation at hydration time.
24
+ */
25
+ export function hydrateNamespaceEntities(
26
+ entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>,
27
+ kinds: ReadonlyMap<string, AnyEntityKindDescriptor>,
28
+ onUnknown: 'carry' | 'fail',
29
+ nsId?: string,
30
+ ): Record<string, Readonly<Record<string, unknown>>> {
31
+ const result: Record<string, Readonly<Record<string, unknown>>> = {};
32
+ for (const [kind, rawMap] of Object.entries(entries)) {
33
+ const descriptor = kinds.get(kind);
34
+ if (descriptor !== undefined) {
35
+ const built: Record<string, unknown> = {};
36
+ for (const [name, value] of Object.entries(rawMap)) {
37
+ built[name] = descriptor.construct(
38
+ blindCast<
39
+ never,
40
+ "value is this kind's descriptor Input: when authoring, the typed entries-input contract produces it; when hydrating, it was validated against descriptor.schema before this loop. The never target is AnyEntityKindDescriptor's erased Input parameter."
41
+ >(value),
42
+ );
43
+ }
44
+ result[kind] = Object.freeze(built);
45
+ } else if (onUnknown === 'carry') {
46
+ result[kind] = Object.freeze(rawMap);
47
+ } else {
48
+ throw new Error(
49
+ `Unknown entries key "${kind}" in namespace "${nsId ?? '?'}"; no hydration factory registered for this entity kind`,
50
+ );
51
+ }
52
+ }
53
+ return result;
54
+ }
package/src/ir/storage.ts CHANGED
@@ -1,8 +1,11 @@
1
+ import { isPlainRecord } from '@prisma-next/contract/is-plain-record';
1
2
  import type { StorageBase } from '@prisma-next/contract/types';
2
3
  import { blindCast } from '@prisma-next/utils/casts';
3
4
  import type { IRNode } from './ir-node';
4
5
  import type { Namespace } from './namespace';
5
6
 
7
+ export { isPlainRecord };
8
+
6
9
  /**
7
10
  * Canonical address for a named entity in Contract IR / Schema IR.
8
11
  *
@@ -52,10 +55,6 @@ export function* elementCoordinates(
52
55
  }
53
56
  }
54
57
 
55
- function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
56
- return typeof value === 'object' && value !== null;
57
- }
58
-
59
58
  /**
60
59
  * Looks up a single entity in a `Storage`-shaped value by its full coordinate.
61
60
  * Returns `undefined` if the namespace, entity kind, or entity name is absent.
@@ -69,9 +68,9 @@ export function entityAt<T = unknown>(
69
68
  const ns = storage.namespaces[coord.namespaceId];
70
69
  if (ns === undefined) return undefined;
71
70
  const entries = ns.entries;
72
- if (!isRecord(entries)) return undefined;
71
+ if (!isPlainRecord(entries)) return undefined;
73
72
  const kindMap = entries[coord.entityKind];
74
- if (!isRecord(kindMap)) return undefined;
73
+ if (!isPlainRecord(kindMap)) return undefined;
75
74
  if (!Object.hasOwn(kindMap, coord.entityName)) return undefined;
76
75
  return blindCast<T | undefined, 'caller asserts the entity type at this coordinate'>(
77
76
  kindMap[coord.entityName],