@bjornpagen/bumbledb 0.3.0 → 0.5.0
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/COOKBOOK.md +246 -92
- package/README.md +31 -16
- package/dist/closed.d.ts +80 -75
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +102 -127
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +37 -7
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +95 -55
- package/dist/db.js.map +1 -1
- package/dist/exhume.d.ts.map +1 -1
- package/dist/exhume.js +1 -14
- package/dist/exhume.js.map +1 -1
- package/dist/face.d.ts +40 -40
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +9 -17
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +46 -15
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +58 -29
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +13 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +2 -1
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +15 -14
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +1 -7
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +33 -6
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +75 -26
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +21 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +20 -3
- package/dist/native.js.map +1 -1
- package/dist/order.d.ts +36 -0
- package/dist/order.d.ts.map +1 -0
- package/dist/order.js +135 -0
- package/dist/order.js.map +1 -0
- package/dist/query/atom.d.ts +76 -28
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +12 -16
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts +5 -8
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +308 -72
- package/dist/query/lower.js.map +1 -1
- package/dist/query/predicate.d.ts.map +1 -1
- package/dist/query/predicate.js +34 -2
- package/dist/query/predicate.js.map +1 -1
- package/dist/query/run.d.ts +15 -5
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +27 -8
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +37 -29
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +19 -47
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +17 -29
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +41 -38
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +7 -31
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +3 -2
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +13 -4
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +75 -8
- package/dist/statements.js.map +1 -1
- package/package.json +2 -5
- package/src/closed.ts +144 -206
- package/src/db.ts +143 -68
- package/src/exhume.ts +1 -15
- package/src/face.ts +38 -48
- package/src/fields.ts +103 -49
- package/src/index.ts +11 -10
- package/src/law.ts +15 -14
- package/src/lower.ts +2 -9
- package/src/marshal.ts +82 -31
- package/src/native.ts +22 -4
- package/src/order.ts +156 -0
- package/src/query/atom.ts +70 -35
- package/src/query/lower.ts +354 -82
- package/src/query/predicate.ts +39 -4
- package/src/query/run.ts +27 -9
- package/src/query/scope.ts +52 -70
- package/src/relation.ts +54 -68
- package/src/schema.ts +7 -33
- package/src/spec.ts +3 -2
- package/src/statements.ts +82 -8
package/src/closed.ts
CHANGED
|
@@ -1,34 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Closed relations (`docs/architecture/10-data-model.md` § closed
|
|
3
3
|
* relations): a vocabulary whose extension is declared in the schema — two
|
|
4
|
-
* tiers, one function.
|
|
5
|
-
* macro's (host-enum analog): handle CONSTANTS on the value
|
|
6
|
-
* (`Kind.Checking`, ids = declaration order, each a BARE `bigint` — no
|
|
7
|
-
* brand), the `fromId` weld, an `id` field descriptor carrying the CLOSED
|
|
8
|
-
* LINKAGE (the roster — pure structure, no declared domain: the laws type
|
|
9
|
-
* the columns, and `schema()` names the id's generator class `"Kind.id"`)
|
|
10
|
-
* for other relations' field blocks (`kind: Kind.id`), payload readback
|
|
11
|
-
* (`Kind.axioms`), and the declared payload column descriptors
|
|
12
|
-
* (`Kind.columns` — the runtime twin of the `Cols` type parameter, which
|
|
13
|
-
* the face layer's structural wall reads). Bare tier: `closed("Kind", ["Checking",
|
|
4
|
+
* tiers, one function. Bare tier: `closed("Kind", ["Checking",
|
|
14
5
|
* "Savings"])`. Payload tier: `closed("Sev", { pages: bool }, { Critical:
|
|
15
6
|
* { pages: true }, ... })` — one call, three arguments (the curried tier-2
|
|
16
7
|
* spelling is DELETED — canonical utterance): the axioms record IS the
|
|
17
8
|
* handle declaration, every handle carrying every column exactly once
|
|
18
|
-
* (type-enforced).
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
9
|
+
* (type-enforced). At the host surface a handle is its NAME — a string
|
|
10
|
+
* literal of the roster's union (the drizzle law: translation, not
|
|
11
|
+
* abstraction; dispatch over a vocabulary is native `switch` narrowing, so
|
|
12
|
+
* no match operator is minted and no handle constants exist — the literal
|
|
13
|
+
* `"Checking"` is the ONE spelling). Handles are pure DATA, not properties
|
|
14
|
+
* of the value, so NO handle name is reserved: a vocabulary may legally
|
|
15
|
+
* contain handles named `match`, `where`, or `id` — the axioms record and
|
|
16
|
+
* the roster are their own namespaces. The value's whole surface: `name`;
|
|
17
|
+
* `id` — the field descriptor carrying the CLOSED LINKAGE (the roster —
|
|
18
|
+
* pure structure, no declared domain: the laws type the columns, and
|
|
19
|
+
* `schema()` names the id's generator class `"Kind.id"`) for other
|
|
20
|
+
* relations' field blocks (`kind: Kind.id`); `data` (the lowering
|
|
21
|
+
* carrier); `axioms` (payload readback, `Kind.axioms`); `columns` (the
|
|
22
|
+
* runtime twin of the `Cols` type parameter, which the face layer's
|
|
23
|
+
* structural wall reads); and — exactly when payload columns exist —
|
|
24
|
+
* `where()`, the ψ-selection surface (`Kind.where({ mastered: true })` as a
|
|
25
|
+
* face source), resolved through the ONE selection machine
|
|
26
|
+
* (`relation.ts::resolveSelection`); the bare tier has no payload columns
|
|
27
|
+
* to select on, so `.where` is absent there, at the type AND on the value.
|
|
28
|
+
* No fact type and no insert surface exist — closed relations are
|
|
29
|
+
* unwritable by construction: the value simply lacks the writable relation
|
|
30
|
+
* shape.
|
|
28
31
|
*/
|
|
29
32
|
|
|
30
33
|
import * as errors from "@superbuilders/errors"
|
|
31
|
-
import type { OneOf } from "#face.ts"
|
|
32
34
|
import {
|
|
33
35
|
type AnyField,
|
|
34
36
|
assertDeclarationOrderKey,
|
|
@@ -37,31 +39,10 @@ import {
|
|
|
37
39
|
type Infer,
|
|
38
40
|
literalOf
|
|
39
41
|
} from "#fields.ts"
|
|
40
|
-
import {
|
|
42
|
+
import type { AnyRelation, RelationField } from "#relation.ts"
|
|
43
|
+
import { resolveSelection, type SelectionBinding, type SelectionInput } from "#relation.ts"
|
|
41
44
|
import type { LiteralSpec } from "#spec.ts"
|
|
42
45
|
|
|
43
|
-
/**
|
|
44
|
-
* The value-surface property names a handle may not shadow — the macro's
|
|
45
|
-
* name-collision diagnostic, here over the closed value's own properties
|
|
46
|
-
* (`relation`/`selection` are reserved so a closed value can never be
|
|
47
|
-
* mistaken for a selected relation by `on()`'s discriminant; `where` is
|
|
48
|
-
* reserved because the payload tier mints the ψ-selection method under
|
|
49
|
-
* exactly that name; `match` because BOTH tiers mint the exhaustive
|
|
50
|
-
* dispatch under exactly that name).
|
|
51
|
-
*/
|
|
52
|
-
const reservedHandleNames: readonly string[] = Object.freeze([
|
|
53
|
-
"name",
|
|
54
|
-
"id",
|
|
55
|
-
"data",
|
|
56
|
-
"axioms",
|
|
57
|
-
"columns",
|
|
58
|
-
"fromId",
|
|
59
|
-
"where",
|
|
60
|
-
"match",
|
|
61
|
-
"relation",
|
|
62
|
-
"selection"
|
|
63
|
-
])
|
|
64
|
-
|
|
65
46
|
/**
|
|
66
47
|
* A payload column of a closed relation: any field descriptor except a
|
|
67
48
|
* fresh-marked one (a vocabulary's rows are ground axioms, never minted).
|
|
@@ -73,7 +54,7 @@ type PayloadField = Exclude<AnyField, { readonly fresh: true }>
|
|
|
73
54
|
* unspellable — the sealed shape mints the synthetic `id` itself (ordinal
|
|
74
55
|
* 0 of the matchable fields), so a declared column named `id` would be
|
|
75
56
|
* shadowed by the synthetic slot everywhere the shape resolves by name
|
|
76
|
-
* (`
|
|
57
|
+
* (`sealedFieldsOf`, the projected face, `spec.rs`'s resolver). The wall is
|
|
77
58
|
* typed here and judged again at construction in {@link mintClosed} — the
|
|
78
59
|
* runtime twin for untyped callers, warmer and earlier than the engine's
|
|
79
60
|
* `DuplicateFieldName` at `Db.create`.
|
|
@@ -126,12 +107,14 @@ interface ClosedCore<Name extends string, Handles extends string, Cols extends R
|
|
|
126
107
|
readonly name: Name
|
|
127
108
|
/**
|
|
128
109
|
* The closed reference descriptor: `kind: Kind.id` in another relation's
|
|
129
|
-
* field block is the reference through which
|
|
130
|
-
* legal in that relation's selections. Pure structure plus the
|
|
131
|
-
*
|
|
132
|
-
*
|
|
110
|
+
* field block is the reference through which handle literals become
|
|
111
|
+
* legal in that relation's selections. Pure structure plus the PRECISE
|
|
112
|
+
* roster (`ClosedIdField<Handles>` — the handle union is the field's
|
|
113
|
+
* value type under `Infer`); the referencing field's domain is law-born:
|
|
114
|
+
* `schema()` computes it from the declared containment (`"Kind.id"`, the
|
|
115
|
+
* generator class).
|
|
133
116
|
*/
|
|
134
|
-
readonly id: ClosedIdField
|
|
117
|
+
readonly id: ClosedIdField<Handles>
|
|
135
118
|
readonly data: ClosedData
|
|
136
119
|
/** Payload readback: handle to its declared column values, bare and structural. */
|
|
137
120
|
readonly axioms: Axioms<Handles, Cols>
|
|
@@ -144,20 +127,18 @@ interface ClosedCore<Name extends string, Handles extends string, Cols extends R
|
|
|
144
127
|
* descriptors in declaration order for the lowering).
|
|
145
128
|
*/
|
|
146
129
|
readonly columns: Cols
|
|
147
|
-
/** The weld: declaration-order id back to its handle, or undefined beyond the roster. */
|
|
148
|
-
fromId(id: bigint): Handles | undefined
|
|
149
130
|
}
|
|
150
131
|
|
|
151
132
|
/**
|
|
152
|
-
* The `where()` argument of a closed relation:
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
133
|
+
* The `where()` argument of a closed relation: EXACTLY the relation
|
|
134
|
+
* surface's {@link SelectionInput}, over the declared payload columns — the
|
|
135
|
+
* ONE selection vocabulary, so a spelling change there (H3's membership
|
|
136
|
+
* arrays) flows through with no local change here. The synthetic `id` is
|
|
137
|
+
* deliberately unspellable ({@link PayloadColumns} refuses an `id` column):
|
|
138
|
+
* an id selection is spelled only as handle literals on the REFERENCING
|
|
139
|
+
* side (the canonical-utterance law).
|
|
157
140
|
*/
|
|
158
|
-
type ClosedSelectionInput<Cols extends Record<string, PayloadField>> =
|
|
159
|
-
readonly [C in keyof Cols]?: Infer<Cols[C]> | OneOf<Infer<Cols[C]>>
|
|
160
|
-
}
|
|
141
|
+
type ClosedSelectionInput<Cols extends Record<string, PayloadField>> = SelectionInput<Cols>
|
|
161
142
|
|
|
162
143
|
/**
|
|
163
144
|
* A closed relation with a ψ selection applied — what `on()` consumes as a
|
|
@@ -190,52 +171,69 @@ interface ClosedSelectable<Name extends string, Handles extends string, Cols ext
|
|
|
190
171
|
}
|
|
191
172
|
|
|
192
173
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
174
|
+
* A closed relation value: the core surface plus — exactly when payload
|
|
175
|
+
* columns exist — `where()` (the bare tier has nothing to select on, so
|
|
176
|
+
* the method is ABSENT there, not merely uncallable). NOTHING else:
|
|
177
|
+
* handles are data on the roster, never properties of the value (the
|
|
178
|
+
* handle constants, the match operator, and the id-to-handle weld died
|
|
179
|
+
* with the bigint era — dispatch is native `switch` narrowing over the
|
|
180
|
+
* handle union).
|
|
200
181
|
*/
|
|
201
|
-
|
|
202
|
-
|
|
182
|
+
type Closed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> = [
|
|
183
|
+
keyof Cols
|
|
184
|
+
] extends [never]
|
|
185
|
+
? ClosedCore<Name, Handles, Cols>
|
|
186
|
+
: ClosedCore<Name, Handles, Cols> & ClosedSelectable<Name, Handles, Cols>
|
|
187
|
+
|
|
188
|
+
/** Any closed relation value, whatever its roster and columns. */
|
|
189
|
+
interface AnyClosed {
|
|
190
|
+
readonly name: string
|
|
191
|
+
readonly id: ClosedIdField
|
|
192
|
+
readonly data: ClosedData
|
|
193
|
+
readonly axioms: Readonly<Record<string, object>>
|
|
194
|
+
readonly columns: Readonly<Record<string, PayloadField>>
|
|
203
195
|
}
|
|
204
196
|
|
|
205
197
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
198
|
+
* THE relation-kind discriminant — the ONE spelling of "is this schema
|
|
199
|
+
* member closed?" (the type tier's twin is the `AnyClosed` conditional
|
|
200
|
+
* arms). A closed relation's runtime description carries its handle
|
|
201
|
+
* roster; an ordinary relation's never does. Every runtime closed/ordinary
|
|
202
|
+
* fork in the SDK judges through this predicate — never a re-spelled
|
|
203
|
+
* structural probe.
|
|
210
204
|
*/
|
|
211
|
-
|
|
212
|
-
|
|
205
|
+
function isClosedMember(member: AnyRelation | AnyClosed): member is AnyClosed {
|
|
206
|
+
return "handles" in member.data
|
|
213
207
|
}
|
|
214
208
|
|
|
215
209
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
210
|
+
* The SEALED field list of a schema member — THE one reader of "what
|
|
211
|
+
* fields does this owner expose": an ordinary relation's declared fields;
|
|
212
|
+
* a closed relation's sealed shape — the synthetic `id` (the value's own
|
|
213
|
+
* roster-carrying descriptor, by identity) at ordinal 0, then the declared
|
|
214
|
+
* payload columns at declared index + 1 (the sealed shift, mirroring the
|
|
215
|
+
* engine's `SchemaDescriptor::sealed_fields`). A `ClosedColumn` is
|
|
216
|
+
* structurally a `RelationField`, so both kinds read uniformly.
|
|
223
217
|
*/
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
: ClosedSelectable<Name, Handles, Cols> & ClosedMatchPayload<Handles, Cols>)
|
|
218
|
+
function sealedFieldsOf(member: AnyRelation | AnyClosed): readonly RelationField[] {
|
|
219
|
+
if (isClosedMember(member)) {
|
|
220
|
+
return Object.freeze([Object.freeze({ name: "id", field: member.id }), ...member.data.columns])
|
|
221
|
+
}
|
|
222
|
+
return member.data.fields
|
|
223
|
+
}
|
|
231
224
|
|
|
232
|
-
/**
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
225
|
+
/**
|
|
226
|
+
* One sealed field by name — derived from {@link sealedFieldsOf}, so the
|
|
227
|
+
* closed synthetic `id` resolves everywhere a name is looked up (no reader
|
|
228
|
+
* can silently lack the `id` arm). `undefined` when the name is foreign
|
|
229
|
+
* (the type tiers make that unwritable; the engine re-judges at
|
|
230
|
+
* `Db.create`).
|
|
231
|
+
*/
|
|
232
|
+
function sealedFieldOf(member: AnyRelation | AnyClosed, fieldName: string): AnyField | undefined {
|
|
233
|
+
const declared = sealedFieldsOf(member).find(function byName(candidate) {
|
|
234
|
+
return candidate.name === fieldName
|
|
235
|
+
})
|
|
236
|
+
return declared?.field
|
|
239
237
|
}
|
|
240
238
|
|
|
241
239
|
/** Narrows the two-tier second argument: a handle tuple (bare tier) or a column block (payload tier). */
|
|
@@ -264,7 +262,8 @@ function handleKeysOwn<Handles extends string>(
|
|
|
264
262
|
* The trusted seam of the axiom-readback mint: every handle carries an own
|
|
265
263
|
* frozen row and every row carries every declared column as an own
|
|
266
264
|
* property — verified before the record is admitted as the typed
|
|
267
|
-
* {@link Axioms} (the
|
|
265
|
+
* {@link Axioms} (the trusted-admission-seam pattern — its home is
|
|
266
|
+
* `isTypedScope` in query/lower.ts).
|
|
268
267
|
*/
|
|
269
268
|
function axiomsMinted<Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
270
269
|
record: Readonly<Record<string, object>>,
|
|
@@ -282,53 +281,22 @@ function axiomsMinted<Handles extends string, Cols extends Record<string, Payloa
|
|
|
282
281
|
})
|
|
283
282
|
}
|
|
284
283
|
|
|
285
|
-
/**
|
|
286
|
-
* The trusted seam of the handle-constant mint: every handle reads back as
|
|
287
|
-
* an own bigint — verified before the record is admitted at the constants
|
|
288
|
-
* type (a "__proto__"-named handle riding the object-protocol accessor
|
|
289
|
-
* would fail exactly this check).
|
|
290
|
-
*/
|
|
291
|
-
function constantsMinted<Handles extends string>(
|
|
292
|
-
record: Readonly<Record<string, bigint>>,
|
|
293
|
-
handles: readonly Handles[]
|
|
294
|
-
): record is Readonly<Record<string, bigint>> & { readonly [H in Handles]: bigint } {
|
|
295
|
-
return handles.every(function constantMinted(handle) {
|
|
296
|
-
return typeof record[handle] === "bigint"
|
|
297
|
-
})
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Reads one handle's ground axiom row for lowering. The typed payload
|
|
302
|
-
* surface makes absence unrepresentable ({@link Axioms} carries every
|
|
303
|
-
* handle's row); the refusal below guards the one ill-typed path — payload
|
|
304
|
-
* columns with the bare tier's absent axioms — which no public spelling
|
|
305
|
-
* reaches.
|
|
306
|
-
*/
|
|
307
|
-
function groundRow<Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
308
|
-
name: string,
|
|
309
|
-
axioms: Axioms<Handles, Cols> | undefined,
|
|
310
|
-
handle: Handles
|
|
311
|
-
): Readonly<Record<string, unknown>> {
|
|
312
|
-
if (axioms === undefined) {
|
|
313
|
-
throw errors.new(`closed relation ${name}: payload columns declared without ground axioms`)
|
|
314
|
-
}
|
|
315
|
-
return axioms[handle]
|
|
316
|
-
}
|
|
317
|
-
|
|
318
284
|
/**
|
|
319
285
|
* Mints the axiom-readback record: one own frozen row per handle (the bare
|
|
320
286
|
* tier's rows are empty — it declares no columns), each row a fresh copy of
|
|
321
|
-
* its ground axiom.
|
|
287
|
+
* its ground axiom. Both tiers supply a REAL axioms record ({@link
|
|
288
|
+
* closedBare} mints its empty rows), so the columns-without-axioms state is
|
|
289
|
+
* unrepresentable here — no undefined arm exists to guard.
|
|
322
290
|
*/
|
|
323
291
|
function mintAxioms<Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
324
292
|
name: string,
|
|
325
293
|
handles: readonly Handles[],
|
|
326
294
|
cols: readonly ClosedColumn[],
|
|
327
|
-
axioms: Axioms<Handles, Cols>
|
|
295
|
+
axioms: Axioms<Handles, Cols>
|
|
328
296
|
): Axioms<Handles, Cols> {
|
|
329
297
|
const out: Record<string, object> = {}
|
|
330
298
|
for (const handle of handles) {
|
|
331
|
-
const row =
|
|
299
|
+
const row = Object.freeze({ ...axioms[handle] })
|
|
332
300
|
Object.defineProperty(out, handle, { value: row, enumerable: true })
|
|
333
301
|
}
|
|
334
302
|
Object.freeze(out)
|
|
@@ -338,22 +306,6 @@ function mintAxioms<Handles extends string, Cols extends Record<string, PayloadF
|
|
|
338
306
|
return out
|
|
339
307
|
}
|
|
340
308
|
|
|
341
|
-
/** Mints the handle constants: one own bigint per handle, ids = declaration order. */
|
|
342
|
-
function mintHandleConstants<Handles extends string>(
|
|
343
|
-
name: string,
|
|
344
|
-
handles: readonly Handles[]
|
|
345
|
-
): { readonly [H in Handles]: bigint } {
|
|
346
|
-
const out: Record<string, bigint> = {}
|
|
347
|
-
handles.forEach(function mintHandleConstant(handle, index) {
|
|
348
|
-
Object.defineProperty(out, handle, { value: BigInt(index), enumerable: true })
|
|
349
|
-
})
|
|
350
|
-
Object.freeze(out)
|
|
351
|
-
if (!constantsMinted(out, handles)) {
|
|
352
|
-
throw errors.new(`closed relation ${name}: handle-constant minting incomplete`)
|
|
353
|
-
}
|
|
354
|
-
return out
|
|
355
|
-
}
|
|
356
|
-
|
|
357
309
|
/** Bare tier: `closed("Kind", ["Checking", "Savings"])` — handles only. */
|
|
358
310
|
function closed<const Name extends string, const Handles extends readonly [string, ...string[]]>(
|
|
359
311
|
name: Name,
|
|
@@ -397,12 +349,29 @@ function closed<const Name extends string, const Cols extends PayloadColumns, Ha
|
|
|
397
349
|
return closedPayload(name, shape, axioms)
|
|
398
350
|
}
|
|
399
351
|
|
|
400
|
-
/**
|
|
352
|
+
/**
|
|
353
|
+
* The bare tier's precisely-typed builder: no columns, and the axioms
|
|
354
|
+
* record is the EMPTY-ROW record over the handle roster (one own frozen
|
|
355
|
+
* `{}` per handle, `__proto__`-safe own-property definition) — the same
|
|
356
|
+
* representation the payload tier carries, so `mintClosed` never sees a
|
|
357
|
+
* tier fork and the columns-without-axioms state stops being spellable.
|
|
358
|
+
*/
|
|
401
359
|
function closedBare<Name extends string, Handles extends string>(
|
|
402
360
|
name: Name,
|
|
403
361
|
handles: readonly [Handles, ...Handles[]]
|
|
404
362
|
): Closed<Name, Handles, Record<never, never>> {
|
|
405
|
-
|
|
363
|
+
const empty: Record<string, object> = {}
|
|
364
|
+
for (const handle of handles) {
|
|
365
|
+
/** A duplicated name mints one row; the roster's own duplicate refusal in {@link mintClosed} stays the judge. */
|
|
366
|
+
if (!Object.hasOwn(empty, handle)) {
|
|
367
|
+
Object.defineProperty(empty, handle, { value: Object.freeze({}), enumerable: true })
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
Object.freeze(empty)
|
|
371
|
+
if (!axiomsMinted<Handles, Record<never, never>>(empty, handles, [])) {
|
|
372
|
+
throw errors.new(`closed relation ${name}: bare-tier axiom-row minting incomplete`)
|
|
373
|
+
}
|
|
374
|
+
return mintClosed<Name, Handles, Record<never, never>>(name, handles, {}, empty)
|
|
406
375
|
}
|
|
407
376
|
|
|
408
377
|
/**
|
|
@@ -429,19 +398,18 @@ function closedPayload<Name extends string, Handles extends string, Cols extends
|
|
|
429
398
|
}
|
|
430
399
|
|
|
431
400
|
/**
|
|
432
|
-
* The trusted seam of the ergonomic-surface mint: `
|
|
433
|
-
* own function
|
|
434
|
-
*
|
|
435
|
-
* (`
|
|
436
|
-
*
|
|
401
|
+
* The trusted seam of the ergonomic-surface mint: `where` reads back as an
|
|
402
|
+
* own function exactly when payload columns exist, and is ABSENT otherwise
|
|
403
|
+
* — the runtime twin of the {@link Closed} type's conditional arm
|
|
404
|
+
* (`ClosedCore` alone vs `ClosedCore & ClosedSelectable`), verified before
|
|
405
|
+
* the minted value is admitted at the conditional type.
|
|
437
406
|
*/
|
|
438
407
|
function surfaceMinted<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
439
|
-
value: ClosedCore<Name, Handles, Cols
|
|
408
|
+
value: ClosedCore<Name, Handles, Cols>,
|
|
440
409
|
cols: readonly ClosedColumn[]
|
|
441
|
-
): value is Closed<Name, Handles, Cols> {
|
|
442
|
-
const matchable = "match" in value && typeof value.match === "function"
|
|
410
|
+
): value is ClosedCore<Name, Handles, Cols> & Closed<Name, Handles, Cols> {
|
|
443
411
|
const selectable = "where" in value && typeof value.where === "function"
|
|
444
|
-
return
|
|
412
|
+
return cols.length > 0 ? selectable : !selectable
|
|
445
413
|
}
|
|
446
414
|
|
|
447
415
|
/**
|
|
@@ -449,15 +417,14 @@ function surfaceMinted<Name extends string, Handles extends string, Cols extends
|
|
|
449
417
|
* typed end to end (a wrong-shaped mint is a compile error here, not a
|
|
450
418
|
* laundered `unknown`): roster checks, eager axiom lowering, the
|
|
451
419
|
* roster-carrying `id` descriptor, the frozen `columns` carrier (the runtime
|
|
452
|
-
* twin of the `Cols` type parameter), the
|
|
453
|
-
* `match()` on both tiers, and — on the payload tier only — the
|
|
420
|
+
* twin of the `Cols` type parameter), and — on the payload tier only — the
|
|
454
421
|
* ψ-selection `where()`.
|
|
455
422
|
*/
|
|
456
423
|
function mintClosed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
|
|
457
424
|
name: Name,
|
|
458
425
|
handles: readonly Handles[],
|
|
459
426
|
columns: Cols,
|
|
460
|
-
axioms: Axioms<Handles, Cols>
|
|
427
|
+
axioms: Axioms<Handles, Cols>
|
|
461
428
|
): Closed<Name, Handles, Cols> {
|
|
462
429
|
if (handles.length === 0) {
|
|
463
430
|
throw errors.new(`closed relation ${name}: at least one handle is required (an empty vocabulary declares nothing)`)
|
|
@@ -468,14 +435,9 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
|
|
|
468
435
|
throw errors.new(`closed relation ${name}: duplicate handle ${handle}`)
|
|
469
436
|
}
|
|
470
437
|
seen.add(handle)
|
|
471
|
-
if (reservedHandleNames.includes(handle)) {
|
|
472
|
-
throw errors.new(
|
|
473
|
-
`closed relation ${name}: handle ${handle} collides with the closed value's own surface (${reservedHandleNames.join(", ")})`
|
|
474
|
-
)
|
|
475
|
-
}
|
|
476
438
|
}
|
|
477
439
|
const handleList: readonly Handles[] = Object.freeze([...handles])
|
|
478
|
-
const roster: ClosedRoster = Object.freeze({ name, handles: handleList })
|
|
440
|
+
const roster: ClosedRoster<Handles> = Object.freeze({ name, handles: handleList })
|
|
479
441
|
const cols: ClosedColumn[] = []
|
|
480
442
|
for (const [columnName, field] of Object.entries(columns)) {
|
|
481
443
|
assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
|
|
@@ -488,8 +450,8 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
|
|
|
488
450
|
}
|
|
489
451
|
Object.freeze(cols)
|
|
490
452
|
const rows: ClosedRow[] = handleList.map(function lowerRow(handle) {
|
|
453
|
+
const row: Readonly<Record<string, unknown>> = axioms[handle]
|
|
491
454
|
const values = cols.map(function lowerAxiomLiteral(column) {
|
|
492
|
-
const row = groundRow(name, axioms, handle)
|
|
493
455
|
return Object.freeze(literalOf(column.field, row[column.name]))
|
|
494
456
|
})
|
|
495
457
|
return Object.freeze({ handle, values: Object.freeze(values) })
|
|
@@ -500,25 +462,18 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
|
|
|
500
462
|
columns: cols,
|
|
501
463
|
rows: Object.freeze(rows)
|
|
502
464
|
})
|
|
503
|
-
const id: ClosedIdField = Object.freeze({ kind: "u64", closed: roster })
|
|
465
|
+
const id: ClosedIdField<Handles> = Object.freeze({ kind: "u64", closed: roster })
|
|
504
466
|
/**
|
|
505
|
-
* Handle names are arbitrary identifiers, so rows
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* value whose type claims a bigint constant but reads back an object.
|
|
512
|
-
* (Object SPREAD is CreateDataProperty by spec, so the copies below are
|
|
513
|
-
* own-property safe for column names too.)
|
|
467
|
+
* Handle names are arbitrary identifiers, so axiom rows are minted with
|
|
468
|
+
* OWN-property definition (inside {@link mintAxioms}), never assignment:
|
|
469
|
+
* a handle named "__proto__" would otherwise ride the Object.prototype
|
|
470
|
+
* accessor — silently swapping the record's prototype instead of
|
|
471
|
+
* creating the row. (Object SPREAD is CreateDataProperty by spec, so the
|
|
472
|
+
* copies below are own-property safe for column names too.)
|
|
514
473
|
*/
|
|
515
474
|
const axiomsOut = mintAxioms<Handles, Cols>(name, handleList, cols, axioms)
|
|
516
|
-
const constants = mintHandleConstants(name, handleList)
|
|
517
475
|
const columnsOut: Cols = { ...columns }
|
|
518
476
|
Object.freeze(columnsOut)
|
|
519
|
-
function fromId(idValue: bigint): Handles | undefined {
|
|
520
|
-
return handleList[Number(idValue)]
|
|
521
|
-
}
|
|
522
477
|
const holder: { value: Closed<Name, Handles, Cols> | undefined } = { value: undefined }
|
|
523
478
|
/**
|
|
524
479
|
* The ψ selection: resolved against the declared payload columns through
|
|
@@ -536,27 +491,9 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
|
|
|
536
491
|
selection: resolveSelection(name, cols, Object.entries(selection))
|
|
537
492
|
})
|
|
538
493
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
* and the chosen arm receives the handle's frozen axiom row (the bare
|
|
543
|
-
* tier's arms are typed to take nothing; the empty row rides along
|
|
544
|
-
* unread). One implementation serves both tiers — the conditional
|
|
545
|
-
* {@link Closed} arm claims the tier-exact arm signature and
|
|
546
|
-
* {@link surfaceMinted} is the trusted seam that admits it.
|
|
547
|
-
*/
|
|
548
|
-
function match<T>(idValue: bigint, arms: { readonly [H in Handles]: (row: AxiomRow<Cols>) => T }): T {
|
|
549
|
-
const handle = fromId(idValue)
|
|
550
|
-
if (handle === undefined) {
|
|
551
|
-
throw errors.new(`closed relation ${name}: match on id ${idValue} misses the roster (${handleList.join(", ")})`)
|
|
552
|
-
}
|
|
553
|
-
return arms[handle](axiomsOut[handle])
|
|
554
|
-
}
|
|
555
|
-
const core = { name, id, data, axioms: axiomsOut, columns: columnsOut, fromId }
|
|
556
|
-
const value: ClosedCore<Name, Handles, Cols> & { readonly [H in Handles]: bigint } =
|
|
557
|
-
cols.length > 0
|
|
558
|
-
? Object.freeze({ ...constants, ...core, where, match })
|
|
559
|
-
: Object.freeze({ ...constants, ...core, match })
|
|
494
|
+
const core = { name, id, data, axioms: axiomsOut, columns: columnsOut }
|
|
495
|
+
const value: ClosedCore<Name, Handles, Cols> =
|
|
496
|
+
cols.length > 0 ? Object.freeze({ ...core, where }) : Object.freeze(core)
|
|
560
497
|
if (!surfaceMinted<Name, Handles, Cols>(value, cols)) {
|
|
561
498
|
throw errors.new(`closed relation ${name}: ergonomic-surface minting incomplete`)
|
|
562
499
|
}
|
|
@@ -574,8 +511,9 @@ export type {
|
|
|
574
511
|
ClosedCore,
|
|
575
512
|
ClosedData,
|
|
576
513
|
ClosedRow,
|
|
514
|
+
ClosedSelectable,
|
|
577
515
|
ClosedSelectionInput,
|
|
578
516
|
PayloadField,
|
|
579
517
|
SelectedClosed
|
|
580
518
|
}
|
|
581
|
-
export { closed }
|
|
519
|
+
export { closed, isClosedMember, sealedFieldOf, sealedFieldsOf }
|