@orkestrel/database 0.0.1 → 0.0.2
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 +43 -10
- package/dist/src/browser/index.d.ts +342 -0
- package/dist/src/browser/index.js +686 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/core/index.cjs +2453 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +1730 -0
- package/dist/src/core/index.d.ts +1730 -11
- package/dist/src/core/index.js +209 -1152
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +836 -153
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1054 -0
- package/dist/src/server/index.d.ts +1054 -5
- package/dist/src/server/index.js +1663 -0
- package/dist/src/server/index.js.map +1 -0
- package/package.json +40 -16
- package/dist/src/core/Clause.d.ts +0 -30
- package/dist/src/core/Cursor.d.ts +0 -21
- package/dist/src/core/Database.d.ts +0 -80
- package/dist/src/core/Query.d.ts +0 -47
- package/dist/src/core/Table.d.ts +0 -69
- package/dist/src/core/constants.d.ts +0 -22
- package/dist/src/core/drivers/MemoryDriver.d.ts +0 -94
- package/dist/src/core/errors.d.ts +0 -38
- package/dist/src/core/factories.d.ts +0 -43
- package/dist/src/core/helpers.d.ts +0 -383
- package/dist/src/core/types.d.ts +0 -739
- package/dist/src/server/compilers.d.ts +0 -169
- package/dist/src/server/drivers/JSONDriver.d.ts +0 -106
- package/dist/src/server/factories.d.ts +0 -31
- package/dist/src/server/helpers.d.ts +0 -222
- package/dist/src/server/types.d.ts +0 -36
package/dist/src/core/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { compileSchema, createContract, isArray, isBoolean, isFiniteNumber, isRecord, isString, objectShape, parseNumber, resolveField } from "@orkestrel/contract";
|
|
2
|
+
import { Emitter } from "@orkestrel/emitter";
|
|
1
3
|
//#region src/core/constants.ts
|
|
2
4
|
/**
|
|
3
5
|
* The primary-key column assumed when {@link TableKeys} does not name one.
|
|
@@ -67,946 +69,6 @@ var DatabaseError = class extends Error {
|
|
|
67
69
|
function isDatabaseError(value) {
|
|
68
70
|
return value instanceof DatabaseError;
|
|
69
71
|
}
|
|
70
|
-
Object.freeze([
|
|
71
|
-
"null",
|
|
72
|
-
"boolean",
|
|
73
|
-
"object",
|
|
74
|
-
"array",
|
|
75
|
-
"number",
|
|
76
|
-
"integer",
|
|
77
|
-
"string"
|
|
78
|
-
]);
|
|
79
|
-
/** Determine whether a value is `null`. */
|
|
80
|
-
function isNull(value) {
|
|
81
|
-
return value === null;
|
|
82
|
-
}
|
|
83
|
-
/** Determine whether a value is `undefined`. */
|
|
84
|
-
function isUndefined(value) {
|
|
85
|
-
return value === void 0;
|
|
86
|
-
}
|
|
87
|
-
/** Determine whether a value is a string. */
|
|
88
|
-
function isString(value) {
|
|
89
|
-
return typeof value === "string";
|
|
90
|
-
}
|
|
91
|
-
/** Determine whether a value is a finite number (excludes `NaN` and `±Infinity`). */
|
|
92
|
-
function isFiniteNumber(value) {
|
|
93
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
94
|
-
}
|
|
95
|
-
/** Determine whether a value is a finite integer (excludes `NaN`, `±Infinity`, and fractional numbers). */
|
|
96
|
-
function isInteger(value) {
|
|
97
|
-
return Number.isInteger(value);
|
|
98
|
-
}
|
|
99
|
-
/** Determine whether a value is a boolean. */
|
|
100
|
-
function isBoolean(value) {
|
|
101
|
-
return typeof value === "boolean";
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Determine whether a value is a non-null object.
|
|
105
|
-
*
|
|
106
|
-
* @remarks
|
|
107
|
-
* `true` for arrays, class instances, plain objects, `Map`, `Set`, etc. — use
|
|
108
|
-
* {@link isRecord} when you need a plain-record check.
|
|
109
|
-
*/
|
|
110
|
-
function isObject(value) {
|
|
111
|
-
return typeof value === "object" && value !== null;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Determine whether a value is a plain record (object literal or null-prototype),
|
|
115
|
-
* not an array or class instance.
|
|
116
|
-
*
|
|
117
|
-
* @remarks
|
|
118
|
-
* Use instead of {@link isObject} to distinguish a plain `{}` /
|
|
119
|
-
* `Object.create(null)` from arrays, `Date`, `Map`, etc. The prototype-chain
|
|
120
|
-
* test is realm-agnostic: rather than comparing against the current realm's
|
|
121
|
-
* `Object.prototype` (which a plain object from another `vm.Context`, iframe,
|
|
122
|
-
* or worker would fail), it accepts any value whose prototype is `null`, OR
|
|
123
|
-
* whose prototype's own prototype is `null` — the shape every plain object
|
|
124
|
-
* has in every realm, since `Object.prototype` itself always sits one step
|
|
125
|
-
* above `null`. Arrays and class instances are still rejected: an array's
|
|
126
|
-
* prototype chain runs through `Array.prototype` before `null`, and a class
|
|
127
|
-
* instance's runs through the class's own prototype. The whole body runs
|
|
128
|
-
* inside `attempt` (AGENTS §14) so a revoked `Proxy` or a hostile
|
|
129
|
-
* `getPrototypeOf` trap cannot escape as a thrown error.
|
|
130
|
-
*/
|
|
131
|
-
function isRecord(value) {
|
|
132
|
-
const outcome = attempt(() => {
|
|
133
|
-
if (!isObject(value) || isArray(value)) return false;
|
|
134
|
-
const prototype = Object.getPrototypeOf(value);
|
|
135
|
-
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
136
|
-
});
|
|
137
|
-
return outcome.success && outcome.value;
|
|
138
|
-
}
|
|
139
|
-
/** Determine whether a value is an array. */
|
|
140
|
-
function isArray(value) {
|
|
141
|
-
return Array.isArray(value);
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Determine whether a value is a cycle-safe JSON value.
|
|
145
|
-
*
|
|
146
|
-
* @remarks
|
|
147
|
-
* Total guard: never throws, returns `false` for cycles, functions, `Date`
|
|
148
|
-
* instances, class instances, `NaN`, and `±Infinity`. Arrays and plain records
|
|
149
|
-
* are walked with an ancestor set so recursive input fails instead of hanging.
|
|
150
|
-
* The whole walk runs inside `attempt` (AGENTS §14): a hostile getter on a
|
|
151
|
-
* record property, or a revoked `Proxy` anywhere in the structure, is caught
|
|
152
|
-
* and yields `false` instead of escaping as a thrown error.
|
|
153
|
-
*
|
|
154
|
-
* @param value - The value to test
|
|
155
|
-
* @returns `true` when the value has a JSON representation
|
|
156
|
-
*
|
|
157
|
-
* @example
|
|
158
|
-
* ```ts
|
|
159
|
-
* isJSONValue({ nested: [1, 'x', null] }) // true
|
|
160
|
-
* isJSONValue(Number.NaN) // false
|
|
161
|
-
* ```
|
|
162
|
-
*/
|
|
163
|
-
function isJSONValue(value) {
|
|
164
|
-
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
165
|
-
const check = (entry) => {
|
|
166
|
-
if (entry === null || isString(entry) || isBoolean(entry) || isFiniteNumber(entry)) return true;
|
|
167
|
-
if (Array.isArray(entry)) {
|
|
168
|
-
if (ancestors.has(entry)) return false;
|
|
169
|
-
ancestors.add(entry);
|
|
170
|
-
const valid = entry.every(check);
|
|
171
|
-
ancestors.delete(entry);
|
|
172
|
-
return valid;
|
|
173
|
-
}
|
|
174
|
-
if (!isRecord(entry)) return false;
|
|
175
|
-
if (ancestors.has(entry)) return false;
|
|
176
|
-
ancestors.add(entry);
|
|
177
|
-
const valid = Object.values(entry).every(check);
|
|
178
|
-
ancestors.delete(entry);
|
|
179
|
-
return valid;
|
|
180
|
-
};
|
|
181
|
-
const outcome = attempt(() => check(value));
|
|
182
|
-
return outcome.success && outcome.value;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Invoke a callback and capture its outcome as a {@link Result}, never letting
|
|
186
|
-
* a throw escape.
|
|
187
|
-
*
|
|
188
|
-
* @remarks
|
|
189
|
-
* The single sanctioned never-throw boundary for the guards (AGENTS §14). The
|
|
190
|
-
* `whereOf`, `lazyOf`, and `transformOf` combinators invoke caller-supplied
|
|
191
|
-
* callbacks *inside* a guard body, yet a guard must NEVER throw — it returns a
|
|
192
|
-
* `boolean`. This converts a throwing callback into a `Failure` so the
|
|
193
|
-
* surrounding guard can treat it as a non-match instead of propagating the
|
|
194
|
-
* exception, written once and shared rather than copy-pasted as ad-hoc
|
|
195
|
-
* `try`/`catch`.
|
|
196
|
-
*
|
|
197
|
-
* @param callback - The callback to invoke with no arguments
|
|
198
|
-
* @returns A `Success` carrying the return value, or a `Failure` carrying the
|
|
199
|
-
* thrown reason normalised to an `Error`
|
|
200
|
-
*
|
|
201
|
-
* @example
|
|
202
|
-
* ```ts
|
|
203
|
-
* const outcome = attempt(() => predicate(value))
|
|
204
|
-
* return outcome.success && outcome.value
|
|
205
|
-
* ```
|
|
206
|
-
*/
|
|
207
|
-
function attempt(callback) {
|
|
208
|
-
try {
|
|
209
|
-
return {
|
|
210
|
-
success: true,
|
|
211
|
-
value: callback()
|
|
212
|
-
};
|
|
213
|
-
} catch (reason) {
|
|
214
|
-
if (reason instanceof Error) return {
|
|
215
|
-
success: false,
|
|
216
|
-
error: reason
|
|
217
|
-
};
|
|
218
|
-
let message = "Unknown thrown value";
|
|
219
|
-
try {
|
|
220
|
-
message = String(reason);
|
|
221
|
-
} catch {}
|
|
222
|
-
return {
|
|
223
|
-
success: false,
|
|
224
|
-
error: new Error(message)
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Resolve a (possibly nested) field value from a record by a key or key path.
|
|
230
|
-
*
|
|
231
|
-
* @remarks
|
|
232
|
-
* A single `string` is ONE key (never split on `.`, so dotted keys are safe); a
|
|
233
|
-
* string array descends left-to-right through nested objects. Intermediates may
|
|
234
|
-
* be any object — records, class instances, or arrays indexed by string. Returns
|
|
235
|
-
* `undefined` the moment a segment is missing or lands on a non-object, so the
|
|
236
|
-
* lookup is total — even against a hostile getter or Proxy trap that throws on
|
|
237
|
-
* read, contained via {@link attempt} so the throw never escapes.
|
|
238
|
-
*
|
|
239
|
-
* @param record - The source record
|
|
240
|
-
* @param path - A property key, or a key path descending into nested objects
|
|
241
|
-
* @returns The resolved value, or `undefined`
|
|
242
|
-
*
|
|
243
|
-
* @example
|
|
244
|
-
* ```ts
|
|
245
|
-
* resolveField({ user: { name: 'Ada' } }, ['user', 'name']) // 'Ada'
|
|
246
|
-
* resolveField({ 'a.b': 1 }, 'a.b') // 1 (one key)
|
|
247
|
-
* resolveField({ a: 1 }, ['a', 'b']) // undefined
|
|
248
|
-
* ```
|
|
249
|
-
*/
|
|
250
|
-
function resolveField(record, path) {
|
|
251
|
-
const keys = isString(path) ? [path] : path;
|
|
252
|
-
let current = record;
|
|
253
|
-
for (const key of keys) {
|
|
254
|
-
if (!isObject(current)) return void 0;
|
|
255
|
-
const container = current;
|
|
256
|
-
const outcome = attempt(() => Reflect.get(container, key));
|
|
257
|
-
if (!outcome.success) return void 0;
|
|
258
|
-
current = outcome.value;
|
|
259
|
-
}
|
|
260
|
-
return current;
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Build a deterministic pseudo-random source seeded from a single number.
|
|
264
|
-
*
|
|
265
|
-
* @remarks
|
|
266
|
-
* A mulberry32 generator — the same seed always yields the same sequence, so
|
|
267
|
-
* generated seed data is reproducible across runs. Used as the default random
|
|
268
|
-
* source for {@link compileGenerator}, seeded from the wall clock so casual
|
|
269
|
-
* callers still get varied output without passing a source themselves.
|
|
270
|
-
*
|
|
271
|
-
* @param seed - The seed for the sequence
|
|
272
|
-
* @returns A {@link RandomFunction} returning values in `[0, 1)`
|
|
273
|
-
*
|
|
274
|
-
* @example
|
|
275
|
-
* ```ts
|
|
276
|
-
* const random = seededRandom(42)
|
|
277
|
-
* random() // always the same first value for seed 42
|
|
278
|
-
* ```
|
|
279
|
-
*/
|
|
280
|
-
function seededRandom(seed) {
|
|
281
|
-
let state = seed >>> 0;
|
|
282
|
-
return () => {
|
|
283
|
-
state = state + 1831565813 >>> 0;
|
|
284
|
-
let t = state;
|
|
285
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
286
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
287
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
function arrayOf(elementGuard) {
|
|
291
|
-
return (value) => {
|
|
292
|
-
if (!isArray(value)) return false;
|
|
293
|
-
const outcome = attempt(() => value.every(elementGuard));
|
|
294
|
-
return outcome.success && outcome.value;
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Build a guard that accepts values identical (via `Object.is`) to one of the
|
|
299
|
-
* provided literal primitives.
|
|
300
|
-
*
|
|
301
|
-
* @example
|
|
302
|
-
* ```ts
|
|
303
|
-
* const isRole = literalOf('admin', 'member', 'guest')
|
|
304
|
-
* isRole('admin') // true
|
|
305
|
-
* isRole('owner') // false
|
|
306
|
-
* ```
|
|
307
|
-
*/
|
|
308
|
-
function literalOf(...literals) {
|
|
309
|
-
return (value) => literals.some((literal) => Object.is(literal, value));
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Build a guard that accepts plain records matching a guard shape.
|
|
313
|
-
*
|
|
314
|
-
* @remarks
|
|
315
|
-
* Three calling modes depending on the `optional` argument:
|
|
316
|
-
* - **No `optional`** — all shape keys required; extra keys rejected.
|
|
317
|
-
* - **`optional: K[]`** — the listed keys are optional; all others required.
|
|
318
|
-
* - **`optional: true`** — every shape key is optional.
|
|
319
|
-
*
|
|
320
|
-
* Key presence is tested with `Object.hasOwn`, so a shape key satisfied only by
|
|
321
|
-
* an inherited prototype member (`toString`, `constructor`, …) counts as absent.
|
|
322
|
-
* A non-object / `null` / array input returns `false` rather than throwing. The
|
|
323
|
-
* extra-key check only inspects `Object.keys` (string keys), so an extra
|
|
324
|
-
* enumerable SYMBOL key is never rejected — intentional, for JSON fidelity, and
|
|
325
|
-
* matches the compiled guard.
|
|
326
|
-
*
|
|
327
|
-
* @example
|
|
328
|
-
* ```ts
|
|
329
|
-
* const isUser = recordOf({ name: isString, age: isNumber })
|
|
330
|
-
* isUser({ name: 'Ada', age: 36 }) // true
|
|
331
|
-
* isUser({ name: 'Ada' }) // false — age missing
|
|
332
|
-
*
|
|
333
|
-
* const isPartial = recordOf({ name: isString, age: isNumber }, ['age'])
|
|
334
|
-
* isPartial({ name: 'Ada' }) // true
|
|
335
|
-
* ```
|
|
336
|
-
*/
|
|
337
|
-
function recordOf(shape, optional) {
|
|
338
|
-
const allowed = /* @__PURE__ */ new Set();
|
|
339
|
-
for (const key in shape) if (Object.prototype.hasOwnProperty.call(shape, key)) allowed.add(key);
|
|
340
|
-
const optionalSet = new Set(optional === true ? [...allowed] : isArray(optional) ? optional.map((key) => String(key)) : []);
|
|
341
|
-
return (value) => {
|
|
342
|
-
if (!isRecord(value)) return false;
|
|
343
|
-
const outcome = attempt(() => {
|
|
344
|
-
for (const key of Object.keys(value)) if (!allowed.has(key)) return false;
|
|
345
|
-
for (const key in shape) {
|
|
346
|
-
if (!Object.prototype.hasOwnProperty.call(shape, key)) continue;
|
|
347
|
-
const present = Object.hasOwn(value, key);
|
|
348
|
-
if (!optionalSet.has(key) && !present) return false;
|
|
349
|
-
if (present) {
|
|
350
|
-
const guard = shape[key];
|
|
351
|
-
if (!guard(value[key])) return false;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return true;
|
|
355
|
-
});
|
|
356
|
-
return outcome.success && outcome.value;
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
function orOf(left, right) {
|
|
360
|
-
return (value) => left(value) || right(value);
|
|
361
|
-
}
|
|
362
|
-
function unionOf(...guards) {
|
|
363
|
-
return (value) => guards.some((guard) => guard(value));
|
|
364
|
-
}
|
|
365
|
-
function intersectionOf(...guards) {
|
|
366
|
-
return (value) => guards.every((guard) => guard(value));
|
|
367
|
-
}
|
|
368
|
-
function whereOf(base, predicate) {
|
|
369
|
-
return (value) => {
|
|
370
|
-
if (!base(value)) return false;
|
|
371
|
-
const outcome = attempt(() => predicate(value));
|
|
372
|
-
return outcome.success && outcome.value;
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Build a guard that accepts finite numbers within an inclusive `[min, max]`
|
|
377
|
-
* range.
|
|
378
|
-
*
|
|
379
|
-
* @remarks
|
|
380
|
-
* Refines {@link isFiniteNumber} with the bound comparison, so `NaN` /
|
|
381
|
-
* `±Infinity` are rejected before any comparison runs. An absent bound never
|
|
382
|
-
* constrains that side. Reused for a number's own value AND, applied to a
|
|
383
|
-
* `.length`, for string and array length refinements — the single source of the
|
|
384
|
-
* bound logic shared by the compiled guard and parser (compilers.ts).
|
|
385
|
-
*
|
|
386
|
-
* @example
|
|
387
|
-
* ```ts
|
|
388
|
-
* const inRange = boundsOf(1, 5)
|
|
389
|
-
* inRange(3) // true
|
|
390
|
-
* inRange(0) // false — below min
|
|
391
|
-
* inRange(6) // false — above max
|
|
392
|
-
*
|
|
393
|
-
* const atLeastTwo = boundsOf(2)
|
|
394
|
-
* atLeastTwo(2) // true — unbounded above
|
|
395
|
-
* ```
|
|
396
|
-
*/
|
|
397
|
-
function boundsOf(min, max) {
|
|
398
|
-
return whereOf(isFiniteNumber, (value) => (min === void 0 || value >= min) && (max === void 0 || value <= max));
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Build a guard that accepts strings satisfying optional length and pattern
|
|
402
|
-
* refinements — `min` / `max` length and a `pattern`.
|
|
403
|
-
*
|
|
404
|
-
* @remarks
|
|
405
|
-
* Composes {@link isString} with {@link boundsOf} on the string's `.length` and
|
|
406
|
-
* an inline `pattern.test` (the same refinement {@link matchOf} performs). When all three options are absent it returns
|
|
407
|
-
* the bare {@link isString} guard (the unconstrained fast path), so an
|
|
408
|
-
* unrefined string leaf pays no wrapping cost. The single source of the string
|
|
409
|
-
* refinement shared by the compiled guard and parser (compilers.ts).
|
|
410
|
-
*
|
|
411
|
-
* @example
|
|
412
|
-
* ```ts
|
|
413
|
-
* const isSlug = stringOf({ min: 1, max: 32, pattern: /^[a-z-]+$/ })
|
|
414
|
-
* isSlug('hello-world') // true
|
|
415
|
-
* isSlug('') // false — below min
|
|
416
|
-
* isSlug('Hello') // false — pattern miss
|
|
417
|
-
*
|
|
418
|
-
* stringOf() // identical to isString
|
|
419
|
-
* ```
|
|
420
|
-
*/
|
|
421
|
-
function stringOf(options) {
|
|
422
|
-
const min = options?.min;
|
|
423
|
-
const max = options?.max;
|
|
424
|
-
const pattern = options?.pattern;
|
|
425
|
-
if (min === void 0 && max === void 0 && pattern === void 0) return isString;
|
|
426
|
-
const withinLength = boundsOf(min, max);
|
|
427
|
-
return whereOf(isString, (value) => withinLength(value.length) && (pattern === void 0 || pattern.test(value)));
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Extend a guard to also allow `null`.
|
|
431
|
-
*
|
|
432
|
-
* @example
|
|
433
|
-
* ```ts
|
|
434
|
-
* const isNullableString = nullableOf(isString)
|
|
435
|
-
* isNullableString('hi') // true
|
|
436
|
-
* isNullableString(null) // true
|
|
437
|
-
* isNullableString(42) // false
|
|
438
|
-
* ```
|
|
439
|
-
*/
|
|
440
|
-
function nullableOf(guard) {
|
|
441
|
-
return (value) => value === null || guard(value);
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Parse an unknown value to a string.
|
|
445
|
-
*
|
|
446
|
-
* @remarks
|
|
447
|
-
* A string is returned unchanged; a finite number is coerced to its decimal
|
|
448
|
-
* string (`42` → `'42'`). `NaN`, `±Infinity`, and every other type → `undefined`.
|
|
449
|
-
*
|
|
450
|
-
* @param value - The value to parse
|
|
451
|
-
* @returns A string, or `undefined`
|
|
452
|
-
*/
|
|
453
|
-
function parseString(value) {
|
|
454
|
-
if (isString(value)) return value;
|
|
455
|
-
if (isFiniteNumber(value)) return String(value);
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Parse an unknown value to a finite number.
|
|
459
|
-
*
|
|
460
|
-
* @remarks
|
|
461
|
-
* A finite number is returned unchanged; a non-blank numeric string is parsed
|
|
462
|
-
* via `Number(...)`. `NaN`, `±Infinity`, blank/non-numeric strings, and every
|
|
463
|
-
* other type → `undefined`.
|
|
464
|
-
*
|
|
465
|
-
* @param value - The value to parse
|
|
466
|
-
* @returns A finite number, or `undefined`
|
|
467
|
-
*/
|
|
468
|
-
function parseNumber(value) {
|
|
469
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
470
|
-
if (isString(value)) {
|
|
471
|
-
if (value.trim() === "") return void 0;
|
|
472
|
-
const parsed = Number(value);
|
|
473
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Parse an unknown value to a finite integer.
|
|
478
|
-
*
|
|
479
|
-
* @remarks
|
|
480
|
-
* Accepts whatever {@link parseNumber} accepts, then requires the result to have
|
|
481
|
-
* no fractional part. `3.14` / `'3.14'` → `undefined`.
|
|
482
|
-
*
|
|
483
|
-
* @param value - The value to parse
|
|
484
|
-
* @returns A finite integer, or `undefined`
|
|
485
|
-
*/
|
|
486
|
-
function parseInteger(value) {
|
|
487
|
-
const parsed = parseNumber(value);
|
|
488
|
-
if (parsed === void 0) return void 0;
|
|
489
|
-
return Number.isInteger(parsed) ? parsed : void 0;
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* Parse an unknown value to a boolean.
|
|
493
|
-
*
|
|
494
|
-
* @remarks
|
|
495
|
-
* A boolean is returned unchanged. The strings `'true'` / `'false'` / `'1'` /
|
|
496
|
-
* `'0'` and the numbers `1` / `0` coerce to the matching boolean. Everything
|
|
497
|
-
* else → `undefined`.
|
|
498
|
-
*
|
|
499
|
-
* @param value - The value to parse
|
|
500
|
-
* @returns A boolean, or `undefined`
|
|
501
|
-
*/
|
|
502
|
-
function parseBoolean(value) {
|
|
503
|
-
if (typeof value === "boolean") return value;
|
|
504
|
-
if (value === "true" || value === "1" || value === 1) return true;
|
|
505
|
-
if (value === "false" || value === "0" || value === 0) return false;
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Parse an unknown value to a plain record — the input reference, never cloned.
|
|
509
|
-
*
|
|
510
|
-
* @param value - The value to parse
|
|
511
|
-
* @returns The record, or `undefined`
|
|
512
|
-
*/
|
|
513
|
-
function parseRecord(value) {
|
|
514
|
-
return isRecord(value) ? value : void 0;
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Validate that a {@link ContractShape} tree is well-formed — a pure recursive
|
|
518
|
-
* prepass run before compilation.
|
|
519
|
-
*
|
|
520
|
-
* @remarks
|
|
521
|
-
* Fail-fast, per AGENTS §12: a malformed shape is a programmer error, so this
|
|
522
|
-
* throws a plain `Error` immediately rather than surfacing as a silently-wrong
|
|
523
|
-
* guard, parser, schema, or generator later. Checks, recursively:
|
|
524
|
-
*
|
|
525
|
-
* - An {@link OptionalShape} is only legal as a direct object-property value —
|
|
526
|
-
* `optionalShape` wrapping an array item, a union variant, another
|
|
527
|
-
* optional/nullable's inner shape, `additionalProperties`, or the top-level
|
|
528
|
-
* shape all throw. An object property IS the one legal placement: its value
|
|
529
|
-
* is unwrapped to `.inner` before recursing, so `.inner` itself is validated
|
|
530
|
-
* as a normal (non-optional-wrapping) shape.
|
|
531
|
-
* - A {@link UnionShape} needs at least one variant; a {@link LiteralShape}
|
|
532
|
-
* needs at least one value and rejects non-finite (`NaN` / `Infinity` /
|
|
533
|
-
* `-Infinity`) number values.
|
|
534
|
-
* - A bounded {@link StringShape} / {@link NumberShape} / {@link ArrayShape}
|
|
535
|
-
* needs `min <= max` when both are set.
|
|
536
|
-
* - An integer {@link NumberShape} (`integer: true`) needs a non-empty integer
|
|
537
|
-
* range: `Math.ceil(min ?? -Infinity) <= Math.floor(max ?? Infinity)`.
|
|
538
|
-
* - `null` / `json` / `raw` / `boolean` are always-valid leaves. Recursion
|
|
539
|
-
* continues into array items, object properties (and `additionalProperties`
|
|
540
|
-
* when it is a shape), union variants, and optional/nullable inner shapes.
|
|
541
|
-
*
|
|
542
|
-
* @param shape - The shape to validate
|
|
543
|
-
* @throws {Error} When the shape is malformed
|
|
544
|
-
*/
|
|
545
|
-
function validateShape(shape) {
|
|
546
|
-
switch (shape.type) {
|
|
547
|
-
case "string":
|
|
548
|
-
if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: a string shape has min greater than max");
|
|
549
|
-
return;
|
|
550
|
-
case "number":
|
|
551
|
-
if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: a number shape has min greater than max");
|
|
552
|
-
if (shape.integer === true) {
|
|
553
|
-
if (Math.ceil(shape.min ?? Number.NEGATIVE_INFINITY) > Math.floor(shape.max ?? Number.POSITIVE_INFINITY)) throw new Error("validateShape: an integer number shape has an empty integer range");
|
|
554
|
-
}
|
|
555
|
-
return;
|
|
556
|
-
case "boolean":
|
|
557
|
-
case "null":
|
|
558
|
-
case "json":
|
|
559
|
-
case "raw": return;
|
|
560
|
-
case "literal":
|
|
561
|
-
if (shape.values.length === 0) throw new Error("validateShape: a literal shape needs at least one value");
|
|
562
|
-
for (const value of shape.values) if (typeof value === "number" && !Number.isFinite(value)) throw new Error("validateShape: a literal shape may not contain non-finite number values");
|
|
563
|
-
return;
|
|
564
|
-
case "array":
|
|
565
|
-
if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: an array shape has min greater than max");
|
|
566
|
-
validateShape(shape.items);
|
|
567
|
-
return;
|
|
568
|
-
case "object": {
|
|
569
|
-
for (const key of Object.keys(shape.properties)) {
|
|
570
|
-
const child = shape.properties[key];
|
|
571
|
-
if (child === void 0) continue;
|
|
572
|
-
validateShape(child.type === "optional" ? child.inner : child);
|
|
573
|
-
}
|
|
574
|
-
const extra = shape.additionalProperties;
|
|
575
|
-
if (extra !== void 0 && extra !== true && extra !== false) validateShape(extra);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
case "union":
|
|
579
|
-
if (shape.variants.length === 0) throw new Error("validateShape: a union shape needs at least one variant");
|
|
580
|
-
for (const variant of shape.variants) validateShape(variant);
|
|
581
|
-
return;
|
|
582
|
-
case "optional": throw new Error("validateShape: an optional shape may only appear as a direct object-property value");
|
|
583
|
-
case "nullable":
|
|
584
|
-
validateShape(shape.inner);
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
/**
|
|
589
|
-
* Compile a {@link ContractShape} into a JSON Schema document.
|
|
590
|
-
*
|
|
591
|
-
* @remarks
|
|
592
|
-
* Object shapes emit `additionalProperties: false` (unless opened) and list only
|
|
593
|
-
* required keys in `required`; nullable shapes emit an `anyOf` with `{ type:
|
|
594
|
-
* 'null' }`. Emission only — it never inspects a runtime value.
|
|
595
|
-
*
|
|
596
|
-
* @param shape - The shape to compile
|
|
597
|
-
* @returns The emitted JSON Schema
|
|
598
|
-
*/
|
|
599
|
-
function compileSchema(shape) {
|
|
600
|
-
switch (shape.type) {
|
|
601
|
-
case "string": return {
|
|
602
|
-
type: "string",
|
|
603
|
-
...shape.min !== void 0 ? { minLength: shape.min } : {},
|
|
604
|
-
...shape.max !== void 0 ? { maxLength: shape.max } : {},
|
|
605
|
-
...shape.pattern !== void 0 ? { pattern: shape.pattern.source } : {},
|
|
606
|
-
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
607
|
-
};
|
|
608
|
-
case "number": return {
|
|
609
|
-
type: shape.integer === true ? "integer" : "number",
|
|
610
|
-
...shape.min !== void 0 ? { minimum: shape.min } : {},
|
|
611
|
-
...shape.max !== void 0 ? { maximum: shape.max } : {},
|
|
612
|
-
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
613
|
-
};
|
|
614
|
-
case "boolean": return {
|
|
615
|
-
type: "boolean",
|
|
616
|
-
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
617
|
-
};
|
|
618
|
-
case "null": return {
|
|
619
|
-
type: "null",
|
|
620
|
-
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
621
|
-
};
|
|
622
|
-
case "json": return { ...shape.description !== void 0 ? { description: shape.description } : {} };
|
|
623
|
-
case "literal": return {
|
|
624
|
-
enum: [...shape.values],
|
|
625
|
-
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
626
|
-
};
|
|
627
|
-
case "array": return {
|
|
628
|
-
type: "array",
|
|
629
|
-
items: compileSchema(shape.items),
|
|
630
|
-
...shape.min !== void 0 ? { minItems: shape.min } : {},
|
|
631
|
-
...shape.max !== void 0 ? { maxItems: shape.max } : {},
|
|
632
|
-
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
633
|
-
};
|
|
634
|
-
case "object": {
|
|
635
|
-
const properties = {};
|
|
636
|
-
const required = [];
|
|
637
|
-
for (const key of Object.keys(shape.properties)) {
|
|
638
|
-
const child = shape.properties[key];
|
|
639
|
-
if (child === void 0) continue;
|
|
640
|
-
properties[key] = compileSchema(child);
|
|
641
|
-
if (child.type !== "optional") required.push(key);
|
|
642
|
-
}
|
|
643
|
-
const extra = shape.additionalProperties;
|
|
644
|
-
const additionalProperties = extra === true ? true : extra !== void 0 && extra !== false ? compileSchema(extra) : false;
|
|
645
|
-
return {
|
|
646
|
-
type: "object",
|
|
647
|
-
...Object.keys(properties).length > 0 ? { properties } : {},
|
|
648
|
-
...required.length > 0 ? { required } : {},
|
|
649
|
-
additionalProperties,
|
|
650
|
-
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
case "union": return {
|
|
654
|
-
...shape.mode === "oneOf" ? { oneOf: shape.variants.map((variant) => compileSchema(variant)) } : { anyOf: shape.variants.map((variant) => compileSchema(variant)) },
|
|
655
|
-
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
656
|
-
};
|
|
657
|
-
case "optional": return compileSchema(shape.inner);
|
|
658
|
-
case "nullable": return { anyOf: [compileSchema(shape.inner), { type: "null" }] };
|
|
659
|
-
case "raw": return shape.schema;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Compile a {@link ContractShape} into a runtime type guard.
|
|
664
|
-
*
|
|
665
|
-
* @remarks
|
|
666
|
-
* Reuses the combinators: `literalOf` for literals, `arrayOf` for arrays,
|
|
667
|
-
* `recordOf` for closed objects, `unionOf` for unions, `nullableOf` for nullable,
|
|
668
|
-
* and `whereOf` for constraint refinement. Like every guard it is total — it
|
|
669
|
-
* never throws (AGENTS §14).
|
|
670
|
-
*
|
|
671
|
-
* @param shape - The shape to compile
|
|
672
|
-
* @returns A guard narrowing to the shape's inferred type
|
|
673
|
-
*/
|
|
674
|
-
function compileGuard(shape) {
|
|
675
|
-
switch (shape.type) {
|
|
676
|
-
case "string": return stringOf({
|
|
677
|
-
min: shape.min,
|
|
678
|
-
max: shape.max,
|
|
679
|
-
pattern: shape.pattern
|
|
680
|
-
});
|
|
681
|
-
case "number": {
|
|
682
|
-
const base = shape.integer === true ? isInteger : isFiniteNumber;
|
|
683
|
-
if (shape.min === void 0 && shape.max === void 0) return base;
|
|
684
|
-
return shape.integer === true ? intersectionOf(isInteger, boundsOf(shape.min, shape.max)) : boundsOf(shape.min, shape.max);
|
|
685
|
-
}
|
|
686
|
-
case "boolean": return isBoolean;
|
|
687
|
-
case "null": return isNull;
|
|
688
|
-
case "json": return isJSONValue;
|
|
689
|
-
case "literal": return literalOf(...shape.values);
|
|
690
|
-
case "array": {
|
|
691
|
-
const base = arrayOf(compileGuard(shape.items));
|
|
692
|
-
if (shape.min === void 0 && shape.max === void 0) return base;
|
|
693
|
-
const withinLength = boundsOf(shape.min, shape.max);
|
|
694
|
-
return whereOf(base, (value) => withinLength(value.length));
|
|
695
|
-
}
|
|
696
|
-
case "object": {
|
|
697
|
-
const map = Object.create(null);
|
|
698
|
-
const optionalKeys = [];
|
|
699
|
-
for (const key of Object.keys(shape.properties)) {
|
|
700
|
-
const child = shape.properties[key];
|
|
701
|
-
if (child === void 0) continue;
|
|
702
|
-
if (child.type === "optional") {
|
|
703
|
-
map[key] = compileGuard(child.inner);
|
|
704
|
-
optionalKeys.push(key);
|
|
705
|
-
} else map[key] = compileGuard(child);
|
|
706
|
-
}
|
|
707
|
-
const extra = shape.additionalProperties;
|
|
708
|
-
if (extra === void 0 || extra === false) return optionalKeys.length > 0 ? recordOf(map, optionalKeys) : recordOf(map);
|
|
709
|
-
const additional = extra === true ? void 0 : compileGuard(extra);
|
|
710
|
-
const required = Object.keys(map).filter((key) => !optionalKeys.includes(key));
|
|
711
|
-
return (value) => {
|
|
712
|
-
if (!isRecord(value)) return false;
|
|
713
|
-
for (const key of required) if (!Object.hasOwn(value, key)) return false;
|
|
714
|
-
const outcome = attempt(() => {
|
|
715
|
-
for (const key of Object.keys(value)) {
|
|
716
|
-
const guard = Object.hasOwn(map, key) ? map[key] : void 0;
|
|
717
|
-
if (guard !== void 0) {
|
|
718
|
-
if (!guard(value[key])) return false;
|
|
719
|
-
} else if (additional !== void 0 && !additional(value[key])) return false;
|
|
720
|
-
}
|
|
721
|
-
return true;
|
|
722
|
-
});
|
|
723
|
-
return outcome.success && outcome.value;
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
case "union": return unionOf(...shape.variants.map((variant) => compileGuard(variant)));
|
|
727
|
-
case "optional": return orOf(isUndefined, compileGuard(shape.inner));
|
|
728
|
-
case "nullable": return nullableOf(compileGuard(shape.inner));
|
|
729
|
-
case "raw": return (_value) => true;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
733
|
-
* Compile a {@link ContractShape} into an input parser.
|
|
734
|
-
*
|
|
735
|
-
* @remarks
|
|
736
|
-
* Reuses the leaf parsers (`parseString` / `parseInteger` / `parseNumber` /
|
|
737
|
-
* `parseBoolean` / `parseRecord`) and coerces structurally. An object fails as a
|
|
738
|
-
* whole on any required-field failure; a union returns a guard-valid value
|
|
739
|
-
* unchanged, otherwise the first variant that both parses and guards wins.
|
|
740
|
-
*
|
|
741
|
-
* After coercing a leaf, it re-applies that leaf's REFINEMENTS through the same
|
|
742
|
-
* combinators `compileGuard` uses — `stringOf` for a string's length/pattern and
|
|
743
|
-
* `boundsOf` for a number's value and an array's length — so a value that coerces
|
|
744
|
-
* but violates a bound parses to `undefined`. The result is full parse↔guard
|
|
745
|
-
* soundness (AGENTS §14): a non-`undefined` parse always satisfies the contract's
|
|
746
|
-
* `is`, refinements included.
|
|
747
|
-
*
|
|
748
|
-
* @param shape - The shape to compile
|
|
749
|
-
* @returns A parser yielding the shape's inferred type or `undefined`
|
|
750
|
-
*/
|
|
751
|
-
function compileParser(shape) {
|
|
752
|
-
switch (shape.type) {
|
|
753
|
-
case "string": {
|
|
754
|
-
if (shape.min === void 0 && shape.max === void 0 && shape.pattern === void 0) return parseString;
|
|
755
|
-
const guard = stringOf({
|
|
756
|
-
min: shape.min,
|
|
757
|
-
max: shape.max,
|
|
758
|
-
pattern: shape.pattern
|
|
759
|
-
});
|
|
760
|
-
return (value) => {
|
|
761
|
-
const parsed = parseString(value);
|
|
762
|
-
return parsed !== void 0 && guard(parsed) ? parsed : void 0;
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
case "number": {
|
|
766
|
-
const base = shape.integer === true ? parseInteger : parseNumber;
|
|
767
|
-
if (shape.min === void 0 && shape.max === void 0) return base;
|
|
768
|
-
const within = boundsOf(shape.min, shape.max);
|
|
769
|
-
return (value) => {
|
|
770
|
-
const parsed = base(value);
|
|
771
|
-
return parsed !== void 0 && within(parsed) ? parsed : void 0;
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
case "boolean": return parseBoolean;
|
|
775
|
-
case "null": return (value) => value === null ? null : void 0;
|
|
776
|
-
case "json": return (value) => isJSONValue(value) ? value : void 0;
|
|
777
|
-
case "literal": {
|
|
778
|
-
const allowed = new Set(shape.values);
|
|
779
|
-
return (value) => {
|
|
780
|
-
if (allowed.has(value)) return value;
|
|
781
|
-
if (isString(value)) {
|
|
782
|
-
const trimmed = value.trim();
|
|
783
|
-
if (allowed.has(trimmed)) return trimmed;
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
case "array": {
|
|
788
|
-
const item = compileParser(shape.items);
|
|
789
|
-
const unbounded = shape.min === void 0 && shape.max === void 0;
|
|
790
|
-
const withinLength = boundsOf(shape.min, shape.max);
|
|
791
|
-
return (value) => {
|
|
792
|
-
if (!isArray(value)) return void 0;
|
|
793
|
-
const result = [];
|
|
794
|
-
for (const entry of value) {
|
|
795
|
-
const parsed = item(entry);
|
|
796
|
-
if (parsed === void 0) return void 0;
|
|
797
|
-
result.push(parsed);
|
|
798
|
-
}
|
|
799
|
-
return unbounded || withinLength(result.length) ? result : void 0;
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
case "object": {
|
|
803
|
-
const entries = [];
|
|
804
|
-
for (const key of Object.keys(shape.properties)) {
|
|
805
|
-
const child = shape.properties[key];
|
|
806
|
-
if (child === void 0) continue;
|
|
807
|
-
const optional = child.type === "optional";
|
|
808
|
-
entries.push({
|
|
809
|
-
key,
|
|
810
|
-
parse: compileParser(optional ? child.inner : child),
|
|
811
|
-
optional
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
const known = new Set(entries.map((entry) => entry.key));
|
|
815
|
-
const extra = shape.additionalProperties;
|
|
816
|
-
const additional = extra === void 0 || extra === false || extra === true ? void 0 : compileParser(extra);
|
|
817
|
-
const open = extra === true || additional !== void 0;
|
|
818
|
-
return (value) => {
|
|
819
|
-
const record = parseRecord(value);
|
|
820
|
-
if (record === void 0) return void 0;
|
|
821
|
-
const outcome = attempt(() => {
|
|
822
|
-
const result = Object.create(null);
|
|
823
|
-
for (const entry of entries) {
|
|
824
|
-
const raw = record[entry.key];
|
|
825
|
-
if (raw === void 0) {
|
|
826
|
-
if (entry.optional) continue;
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
const parsed = entry.parse(raw);
|
|
830
|
-
if (parsed === void 0) return void 0;
|
|
831
|
-
result[entry.key] = parsed;
|
|
832
|
-
}
|
|
833
|
-
if (open) for (const key of Object.keys(record)) {
|
|
834
|
-
if (known.has(key)) continue;
|
|
835
|
-
if (additional === void 0) result[key] = record[key];
|
|
836
|
-
else {
|
|
837
|
-
const parsed = additional(record[key]);
|
|
838
|
-
if (parsed === void 0) return void 0;
|
|
839
|
-
result[key] = parsed;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
return result;
|
|
843
|
-
});
|
|
844
|
-
return outcome.success ? outcome.value : void 0;
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
case "union": {
|
|
848
|
-
const variants = shape.variants.map((variant) => ({
|
|
849
|
-
parse: compileParser(variant),
|
|
850
|
-
guard: compileGuard(variant)
|
|
851
|
-
}));
|
|
852
|
-
return (value) => {
|
|
853
|
-
for (const variant of variants) if (variant.guard(value)) return value;
|
|
854
|
-
for (const variant of variants) {
|
|
855
|
-
const parsed = variant.parse(value);
|
|
856
|
-
if (parsed !== void 0 && variant.guard(parsed)) return parsed;
|
|
857
|
-
}
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
case "optional": {
|
|
861
|
-
const inner = compileParser(shape.inner);
|
|
862
|
-
return (value) => value === void 0 ? void 0 : inner(value);
|
|
863
|
-
}
|
|
864
|
-
case "nullable": {
|
|
865
|
-
const inner = compileParser(shape.inner);
|
|
866
|
-
return (value) => value === null ? null : inner(value);
|
|
867
|
-
}
|
|
868
|
-
case "raw": return (value) => value;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
/**
|
|
872
|
-
* Compile a {@link ContractShape} into a deterministic seed value.
|
|
873
|
-
*
|
|
874
|
-
* @remarks
|
|
875
|
-
* The same shape and the same `random` source always produce the same value, so
|
|
876
|
-
* seed data is reproducible. Defaults to a {@link seededRandom} source seeded
|
|
877
|
-
* from the wall clock when none is supplied. Throws on a degenerate empty
|
|
878
|
-
* `literalShape` / `unionShape`, on a pattern-constrained `stringShape` whose
|
|
879
|
-
* generated sample cannot satisfy the pattern, or on a `rawShape` (its embedded
|
|
880
|
-
* schema is arbitrary and cannot be auto-generated) — a programmer error that
|
|
881
|
-
* cannot generate a value (AGENTS §12). `createContract` runs
|
|
882
|
-
* {@link validateShape} first, so a degenerate `literalShape` / `unionShape` /
|
|
883
|
-
* bounded shape is normally caught there; these throws remain here as defense
|
|
884
|
-
* for standalone `compileGenerator` use.
|
|
885
|
-
*
|
|
886
|
-
* @param shape - The shape to generate from
|
|
887
|
-
* @param random - A seeded random source (defaults to `seededRandom(Date.now())`)
|
|
888
|
-
* @returns A value matching the shape
|
|
889
|
-
*/
|
|
890
|
-
function compileGenerator(shape, random = seededRandom(Date.now())) {
|
|
891
|
-
switch (shape.type) {
|
|
892
|
-
case "string": {
|
|
893
|
-
const min = shape.min ?? 0;
|
|
894
|
-
const max = shape.max ?? Math.max(min, 12);
|
|
895
|
-
const length = Math.max(min, Math.min(max, 8));
|
|
896
|
-
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
897
|
-
let value = "";
|
|
898
|
-
for (let index = 0; index < length; index += 1) value += alphabet[Math.floor(random() * 36)];
|
|
899
|
-
if (shape.pattern !== void 0 && !shape.pattern.test(value)) throw new Error("compileGenerator: a pattern-constrained string shape cannot be auto-generated — supply or verify values another way");
|
|
900
|
-
return value;
|
|
901
|
-
}
|
|
902
|
-
case "number": {
|
|
903
|
-
const min = shape.min ?? 0;
|
|
904
|
-
const max = shape.max ?? 100;
|
|
905
|
-
if (shape.integer === true) {
|
|
906
|
-
const lo = Math.ceil(min);
|
|
907
|
-
const hi = Math.floor(max);
|
|
908
|
-
return Math.floor(random() * (hi - lo + 1)) + lo;
|
|
909
|
-
}
|
|
910
|
-
return random() * (max - min) + min;
|
|
911
|
-
}
|
|
912
|
-
case "boolean": return random() >= .5;
|
|
913
|
-
case "null": return null;
|
|
914
|
-
case "json": {
|
|
915
|
-
const pick = Math.floor(random() * 5);
|
|
916
|
-
if (pick === 0) return null;
|
|
917
|
-
if (pick === 1) return random() >= .5;
|
|
918
|
-
if (pick === 2) return Math.floor(random() * 1e3);
|
|
919
|
-
if (pick === 3) {
|
|
920
|
-
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
|
921
|
-
let value = "";
|
|
922
|
-
for (let index = 0; index < 6; index += 1) value += alphabet[Math.floor(random() * 26)];
|
|
923
|
-
return value;
|
|
924
|
-
}
|
|
925
|
-
return { value: Math.floor(random() * 1e3) };
|
|
926
|
-
}
|
|
927
|
-
case "literal":
|
|
928
|
-
if (shape.values.length === 0) throw new Error("compileGenerator: a literal shape needs at least one value");
|
|
929
|
-
return shape.values[Math.floor(random() * shape.values.length)];
|
|
930
|
-
case "array": {
|
|
931
|
-
const lo = shape.min ?? Math.min(1, shape.max ?? 1);
|
|
932
|
-
const hi = shape.max ?? Math.max(lo, 3);
|
|
933
|
-
const length = Math.floor(random() * (hi - lo + 1)) + lo;
|
|
934
|
-
const result = [];
|
|
935
|
-
for (let index = 0; index < length; index += 1) result.push(compileGenerator(shape.items, random));
|
|
936
|
-
return result;
|
|
937
|
-
}
|
|
938
|
-
case "object": {
|
|
939
|
-
const result = {};
|
|
940
|
-
for (const key of Object.keys(shape.properties)) {
|
|
941
|
-
const child = shape.properties[key];
|
|
942
|
-
if (child === void 0) continue;
|
|
943
|
-
if (child.type === "optional" && random() < .3) continue;
|
|
944
|
-
result[key] = compileGenerator(child, random);
|
|
945
|
-
}
|
|
946
|
-
const extra = shape.additionalProperties;
|
|
947
|
-
if (extra !== void 0 && extra !== true && extra !== false) {
|
|
948
|
-
const count = 1 + Math.floor(random() * 2);
|
|
949
|
-
for (let index = 0; index < count; index += 1) {
|
|
950
|
-
const key = `key${index}`;
|
|
951
|
-
if (Object.hasOwn(result, key)) continue;
|
|
952
|
-
result[key] = compileGenerator(extra, random);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
return result;
|
|
956
|
-
}
|
|
957
|
-
case "union":
|
|
958
|
-
if (shape.variants.length === 0) throw new Error("compileGenerator: a union shape needs at least one variant");
|
|
959
|
-
return compileGenerator(shape.variants[Math.floor(random() * shape.variants.length)], random);
|
|
960
|
-
case "optional": return compileGenerator(shape.inner, random);
|
|
961
|
-
case "nullable": return random() < .2 ? null : compileGenerator(shape.inner, random);
|
|
962
|
-
case "raw": throw new Error("compileGenerator: a raw shape embeds an arbitrary JSON Schema and cannot be auto-generated — supply values another way");
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
function createContract(shape) {
|
|
966
|
-
validateShape(shape);
|
|
967
|
-
const schema = compileSchema(shape);
|
|
968
|
-
const guard = compileGuard(shape);
|
|
969
|
-
const parser = compileParser(shape);
|
|
970
|
-
return {
|
|
971
|
-
schema,
|
|
972
|
-
is: guard,
|
|
973
|
-
parse(value) {
|
|
974
|
-
return parser(value);
|
|
975
|
-
},
|
|
976
|
-
generate(random) {
|
|
977
|
-
return compileGenerator(shape, random);
|
|
978
|
-
}
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
/**
|
|
982
|
-
* Build an {@link ObjectShape} from a property map.
|
|
983
|
-
*
|
|
984
|
-
* @remarks
|
|
985
|
-
* Wrap any property in {@link optionalShape} to allow its absence. By default
|
|
986
|
-
* the compiled guard rejects unknown keys; pass `additionalProperties` to open
|
|
987
|
-
* the object.
|
|
988
|
-
*
|
|
989
|
-
* @param properties - Map of property names to child shapes
|
|
990
|
-
* @param options - Optional `additionalProperties` and `description`
|
|
991
|
-
* @returns An object shape
|
|
992
|
-
*
|
|
993
|
-
* @example
|
|
994
|
-
* ```ts
|
|
995
|
-
* const user = objectShape({
|
|
996
|
-
* name: stringShape({ min: 1 }),
|
|
997
|
-
* age: integerShape({ min: 0, max: 120 }),
|
|
998
|
-
* bio: optionalShape(stringShape()),
|
|
999
|
-
* })
|
|
1000
|
-
* ```
|
|
1001
|
-
*/
|
|
1002
|
-
function objectShape(properties, options) {
|
|
1003
|
-
return {
|
|
1004
|
-
type: "object",
|
|
1005
|
-
properties,
|
|
1006
|
-
additionalProperties: options?.additionalProperties,
|
|
1007
|
-
description: options?.description
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
72
|
//#endregion
|
|
1011
73
|
//#region src/core/helpers.ts
|
|
1012
74
|
/**
|
|
@@ -1153,9 +215,16 @@ function globMatch(value, pattern) {
|
|
|
1153
215
|
* @remarks
|
|
1154
216
|
* Reads the condition's column — a `FieldPath`, resolved with `resolveField` (a
|
|
1155
217
|
* string is one column; an array descends a nested value) — and applies the
|
|
1156
|
-
* operator. Range operators
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
218
|
+
* operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
|
|
219
|
+
* {@link compareValues}, the total order; the equality family (`equals` / `not`
|
|
220
|
+
* / `any` / `none`) uses {@link deepEqual} — STRUCTURAL equality, not the total
|
|
221
|
+
* order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
|
|
222
|
+
* operand only matches a structurally-equal value, never every row holding any
|
|
223
|
+
* object. This is a semantics change from ranking: `deepEqual` is SameValueZero
|
|
224
|
+
* on leaves, so `NaN` now equals `NaN` under `equals` / `any` (it never matched
|
|
225
|
+
* anything under the old rank-based comparison). `like` / `glob` / `starts` /
|
|
226
|
+
* `ends` match only strings; `absent` / `present` test nullishness. Total — a
|
|
227
|
+
* type mismatch is simply a non-match.
|
|
1159
228
|
*
|
|
1160
229
|
* @param row - The row to test
|
|
1161
230
|
* @param condition - The condition to apply
|
|
@@ -1166,8 +235,8 @@ function matchesCondition(row, condition) {
|
|
|
1166
235
|
const first = condition.values[0];
|
|
1167
236
|
const second = condition.values[1];
|
|
1168
237
|
switch (condition.operator) {
|
|
1169
|
-
case "equals": return
|
|
1170
|
-
case "not": return
|
|
238
|
+
case "equals": return deepEqual(value, first);
|
|
239
|
+
case "not": return !deepEqual(value, first);
|
|
1171
240
|
case "above": return compareValues(value, first) > 0;
|
|
1172
241
|
case "below": return compareValues(value, first) < 0;
|
|
1173
242
|
case "from": return compareValues(value, first) >= 0;
|
|
@@ -1177,8 +246,8 @@ function matchesCondition(row, condition) {
|
|
|
1177
246
|
case "glob": return isString(value) && isString(first) && globMatch(value, first);
|
|
1178
247
|
case "starts": return isString(value) && isString(first) && value.startsWith(first);
|
|
1179
248
|
case "ends": return isString(value) && isString(first) && value.endsWith(first);
|
|
1180
|
-
case "any": return condition.values.some((candidate) =>
|
|
1181
|
-
case "none": return !condition.values.some((candidate) =>
|
|
249
|
+
case "any": return condition.values.some((candidate) => deepEqual(value, candidate));
|
|
250
|
+
case "none": return !condition.values.some((candidate) => deepEqual(value, candidate));
|
|
1182
251
|
case "absent": return value === void 0 || value === null;
|
|
1183
252
|
case "present": return value !== void 0 && value !== null;
|
|
1184
253
|
}
|
|
@@ -1363,6 +432,44 @@ function shapeToColumnType(shape) {
|
|
|
1363
432
|
}
|
|
1364
433
|
}
|
|
1365
434
|
/**
|
|
435
|
+
* Whether a value is a well-formed {@link DriverMeta} — the boundary guard a
|
|
436
|
+
* versioning driver's `meta()` narrows a stored (structured-clone or
|
|
437
|
+
* `JSON.parse`d) value through before trusting it, replacing the per-driver
|
|
438
|
+
* duplicated narrowing every backend used to hand-roll (AGENTS §14: never `as`).
|
|
439
|
+
*
|
|
440
|
+
* @remarks
|
|
441
|
+
* Total and total-recursive over the whole shape: a finite `version`, and a
|
|
442
|
+
* `schema` array of well-formed {@link TableSchema} entries — each a `name` /
|
|
443
|
+
* `primary` string pair, a `columns` array of well-formed {@link ColumnSchema}
|
|
444
|
+
* entries (a `name` string, a {@link ColumnType} literal, a `nullable`
|
|
445
|
+
* boolean), and an `indexes` array of string arrays. Anything off-shape
|
|
446
|
+
* (including a non-record) returns `false` rather than throwing.
|
|
447
|
+
*
|
|
448
|
+
* @param value - The value to test
|
|
449
|
+
* @returns `true` when `value` is a well-formed `DriverMeta`
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```ts
|
|
453
|
+
* isDriverMeta({ version: 1, schema: [] }) // true
|
|
454
|
+
* isDriverMeta({ version: 1, schema: [{ name: 'users' }] }) // false
|
|
455
|
+
* ```
|
|
456
|
+
*/
|
|
457
|
+
function isDriverMeta(value) {
|
|
458
|
+
const COLUMN_TYPES = [
|
|
459
|
+
"text",
|
|
460
|
+
"integer",
|
|
461
|
+
"real",
|
|
462
|
+
"boolean",
|
|
463
|
+
"json",
|
|
464
|
+
"blob"
|
|
465
|
+
];
|
|
466
|
+
const isColumnType = (candidate) => isString(candidate) && COLUMN_TYPES.some((type) => type === candidate);
|
|
467
|
+
const isColumnSchema = (candidate) => isRecord(candidate) && isString(candidate.name) && isColumnType(candidate.type) && isBoolean(candidate.nullable);
|
|
468
|
+
const isIndexGroup = (candidate) => isArray(candidate) && candidate.every((entry) => isString(entry));
|
|
469
|
+
const isTableSchema = (candidate) => isRecord(candidate) && isString(candidate.name) && isString(candidate.primary) && isArray(candidate.columns) && candidate.columns.every(isColumnSchema) && isArray(candidate.indexes) && candidate.indexes.every(isIndexGroup);
|
|
470
|
+
return isRecord(value) && isFiniteNumber(value.version) && isArray(value.schema) && value.schema.every(isTableSchema);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
1366
473
|
* Throw when an {@link ReadOptions.signal | AbortSignal} has fired — the shared
|
|
1367
474
|
* cancellation gate checked at operation boundaries and between streamed rows.
|
|
1368
475
|
*
|
|
@@ -1406,11 +513,22 @@ function checkAbort(signal) {
|
|
|
1406
513
|
* plan labels only — version tracking itself is deferred to persistent
|
|
1407
514
|
* backends.
|
|
1408
515
|
*
|
|
516
|
+
* A column present in BOTH schemas under the same name but with a different
|
|
517
|
+
* `type` or `nullable` throws a `MIGRATION` {@link DatabaseError} naming the
|
|
518
|
+
* table, the column, and the from→to difference — a name-only diff would
|
|
519
|
+
* otherwise silently produce NO step for the drift, and versioned
|
|
520
|
+
* reconciliation would stamp over it. There is no automatic in-place
|
|
521
|
+
* type-change step: the manual path is to add a new column, copy/convert the
|
|
522
|
+
* data at the application layer, then remove the old column — two separate
|
|
523
|
+
* plans, never a single implicit "alter" step.
|
|
524
|
+
*
|
|
1409
525
|
* @param deployed - The table schemas currently applied
|
|
1410
526
|
* @param declared - The table schemas the caller wants applied
|
|
1411
527
|
* @param from - The plan's source version label (defaults to `0`)
|
|
1412
528
|
* @param to - The plan's target version label (defaults to `1`)
|
|
1413
529
|
* @returns The migration plan moving `deployed` toward `declared`
|
|
530
|
+
* @throws A `MIGRATION` {@link DatabaseError} when a shared column's `type` or
|
|
531
|
+
* `nullable` differs between `deployed` and `declared`
|
|
1414
532
|
*
|
|
1415
533
|
* @example
|
|
1416
534
|
* ```ts
|
|
@@ -1443,11 +561,29 @@ function planMigration(deployed, declared, from = 0, to = 1) {
|
|
|
1443
561
|
table: table.name,
|
|
1444
562
|
column: column.name
|
|
1445
563
|
});
|
|
1446
|
-
for (const column of table.columns)
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
564
|
+
for (const column of table.columns) {
|
|
565
|
+
const previous = beforeColumns.get(column.name);
|
|
566
|
+
if (previous === void 0) {
|
|
567
|
+
steps.push({
|
|
568
|
+
operation: "column.add",
|
|
569
|
+
table: table.name,
|
|
570
|
+
column
|
|
571
|
+
});
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (previous.type !== column.type || previous.nullable !== column.nullable) throw new DatabaseError("MIGRATION", `planMigration: column '${column.name}' on table '${table.name}' changed shape (type ${previous.type}→${column.type}, nullable ${previous.nullable}→${column.nullable}) — in-place type/nullability changes are not auto-migrated; add a new column, copy/convert the data, then remove the old column`, {
|
|
575
|
+
table: table.name,
|
|
576
|
+
column: column.name,
|
|
577
|
+
from: {
|
|
578
|
+
type: previous.type,
|
|
579
|
+
nullable: previous.nullable
|
|
580
|
+
},
|
|
581
|
+
to: {
|
|
582
|
+
type: column.type,
|
|
583
|
+
nullable: column.nullable
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
1451
587
|
const sameIndex = (left, right) => left.length === right.length && left.every((column, position) => column === right[position]);
|
|
1452
588
|
for (const index of before.indexes) if (!table.indexes.some((candidate) => sameIndex(candidate, index))) steps.push({
|
|
1453
589
|
operation: "index.remove",
|
|
@@ -1510,12 +646,14 @@ function migrateRows(rows, steps) {
|
|
|
1510
646
|
* default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
|
|
1511
647
|
* fresh for each phase so failures stay isolated, verifies: `open`/`close`;
|
|
1512
648
|
* `read` of a missing key returns `undefined`; `write`/`read` round-trip with
|
|
1513
|
-
* copy-in/copy-out isolation (mutating the caller's row
|
|
1514
|
-
* row `read` returns, never perturbs
|
|
1515
|
-
* `delete` returns `true` then `false`;
|
|
1516
|
-
* order; `clear` empties only its target
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
649
|
+
* DEEP copy-in/copy-out isolation (mutating the caller's row — including a
|
|
650
|
+
* NESTED field — after `write`, or a row `read` returns, never perturbs
|
|
651
|
+
* stored state) and upsert-overwrite; `delete` returns `true` then `false`;
|
|
652
|
+
* `keys`/`scan` yield in ascending key order; `clear` empties only its target
|
|
653
|
+
* table; `snapshot`'s rollback thunk restores pre-snapshot state, including a
|
|
654
|
+
* NESTED field mutated in place on a read-back row between capture and
|
|
655
|
+
* restore; a scoped `snapshot(['users'])` rolls back only the named table,
|
|
656
|
+
* leaving a concurrent mutation to another table intact; a
|
|
1519
657
|
* non-`id` primary key (`posts.slug`) round-trips; a nested-object row
|
|
1520
658
|
* round-trips structurally (via {@link deepEqual}). The optional surface is
|
|
1521
659
|
* presence-gated: when `migrate` exists, a `column.remove` plan strips the
|
|
@@ -1568,11 +706,16 @@ async function* driverFindings(factory) {
|
|
|
1568
706
|
name: "age",
|
|
1569
707
|
type: "integer",
|
|
1570
708
|
nullable: true
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
name: "meta",
|
|
712
|
+
type: "json",
|
|
713
|
+
nullable: true
|
|
1571
714
|
}
|
|
1572
715
|
],
|
|
1573
716
|
indexes: []
|
|
1574
717
|
};
|
|
1575
|
-
const
|
|
718
|
+
const CONFORMANCE_POSTS_SCHEMA = {
|
|
1576
719
|
name: "posts",
|
|
1577
720
|
primary: "slug",
|
|
1578
721
|
columns: [{
|
|
@@ -1585,7 +728,8 @@ async function* driverFindings(factory) {
|
|
|
1585
728
|
nullable: false
|
|
1586
729
|
}],
|
|
1587
730
|
indexes: []
|
|
1588
|
-
}
|
|
731
|
+
};
|
|
732
|
+
const CONFORMANCE_SCHEMA = [CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA];
|
|
1589
733
|
const findingOf = (check, message, context) => ({
|
|
1590
734
|
check,
|
|
1591
735
|
message,
|
|
@@ -1622,29 +766,33 @@ async function* driverFindings(factory) {
|
|
|
1622
766
|
const input = {
|
|
1623
767
|
id: "u1",
|
|
1624
768
|
name: "Ada",
|
|
1625
|
-
age: 30
|
|
769
|
+
age: 30,
|
|
770
|
+
meta: { tags: ["a"] }
|
|
1626
771
|
};
|
|
1627
772
|
await driver.write("users", "u1", input);
|
|
1628
773
|
input.name = "Mutated after write";
|
|
774
|
+
if (isRecord(input.meta) && Array.isArray(input.meta.tags)) input.meta.tags.push("mutated");
|
|
1629
775
|
const stored = await driver.read("users", "u1");
|
|
1630
776
|
const original = {
|
|
1631
777
|
id: "u1",
|
|
1632
778
|
name: "Ada",
|
|
1633
|
-
age: 30
|
|
779
|
+
age: 30,
|
|
780
|
+
meta: { tags: ["a"] }
|
|
1634
781
|
};
|
|
1635
782
|
if (stored === void 0 || !deepEqual(stored, original)) {
|
|
1636
783
|
await driver.close();
|
|
1637
|
-
return findingOf("copy-in", "write must copy the input row rather than store it by reference", {
|
|
784
|
+
return findingOf("copy-in", "write must deep-copy the input row (including nested fields) rather than store it by reference", {
|
|
1638
785
|
table: "users",
|
|
1639
786
|
expected: original,
|
|
1640
787
|
actual: stored
|
|
1641
788
|
});
|
|
1642
789
|
}
|
|
1643
790
|
stored.name = "Mutated after read";
|
|
791
|
+
if (isRecord(stored.meta) && Array.isArray(stored.meta.tags)) stored.meta.tags.push("mutated");
|
|
1644
792
|
const reread = await driver.read("users", "u1");
|
|
1645
793
|
if (reread === void 0 || !deepEqual(reread, original)) {
|
|
1646
794
|
await driver.close();
|
|
1647
|
-
return findingOf("copy-out", "read must copy the stored row rather than return it by reference", {
|
|
795
|
+
return findingOf("copy-out", "read must deep-copy the stored row (including nested fields) rather than return it by reference", {
|
|
1648
796
|
table: "users",
|
|
1649
797
|
expected: original,
|
|
1650
798
|
actual: reread
|
|
@@ -1807,6 +955,37 @@ async function* driverFindings(factory) {
|
|
|
1807
955
|
});
|
|
1808
956
|
}
|
|
1809
957
|
},
|
|
958
|
+
{
|
|
959
|
+
check: "snapshot-nested",
|
|
960
|
+
run: async () => {
|
|
961
|
+
const driver = factory();
|
|
962
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
963
|
+
const original = {
|
|
964
|
+
id: "u3",
|
|
965
|
+
name: "Nested",
|
|
966
|
+
age: 20,
|
|
967
|
+
meta: { tags: ["a"] }
|
|
968
|
+
};
|
|
969
|
+
await driver.write("users", "u3", original);
|
|
970
|
+
const rollback = await driver.snapshot();
|
|
971
|
+
const before = await driver.read("users", "u3");
|
|
972
|
+
if (isRecord(before) && isRecord(before.meta) && Array.isArray(before.meta.tags)) before.meta.tags.push("mutated-before-restore");
|
|
973
|
+
await driver.write("users", "u3", {
|
|
974
|
+
id: "u3",
|
|
975
|
+
name: "Nested",
|
|
976
|
+
age: 20,
|
|
977
|
+
meta: { tags: ["a", "mutated-after-write"] }
|
|
978
|
+
});
|
|
979
|
+
await rollback();
|
|
980
|
+
const restored = await driver.read("users", "u3");
|
|
981
|
+
await driver.close();
|
|
982
|
+
if (restored === void 0 || !deepEqual(restored, original)) return findingOf("snapshot-nested", "snapshot rollback must restore pre-snapshot nested field values, unaffected by a later in-place mutation of a read-back row", {
|
|
983
|
+
table: "users",
|
|
984
|
+
expected: original,
|
|
985
|
+
actual: restored
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
},
|
|
1810
989
|
{
|
|
1811
990
|
check: "non-id-primary",
|
|
1812
991
|
run: async () => {
|
|
@@ -1855,21 +1034,22 @@ async function* driverFindings(factory) {
|
|
|
1855
1034
|
run: async () => {
|
|
1856
1035
|
const driver = factory();
|
|
1857
1036
|
if (driver.migrate === void 0) return void 0;
|
|
1858
|
-
|
|
1037
|
+
const deployedUsers = {
|
|
1038
|
+
...CONFORMANCE_USERS_SCHEMA,
|
|
1039
|
+
columns: [...CONFORMANCE_USERS_SCHEMA.columns, {
|
|
1040
|
+
name: "legacy",
|
|
1041
|
+
type: "boolean",
|
|
1042
|
+
nullable: true
|
|
1043
|
+
}]
|
|
1044
|
+
};
|
|
1045
|
+
await driver.open([deployedUsers, CONFORMANCE_POSTS_SCHEMA]);
|
|
1859
1046
|
await driver.write("users", "u1", {
|
|
1860
1047
|
id: "u1",
|
|
1861
1048
|
name: "Ada",
|
|
1862
1049
|
age: 30,
|
|
1863
1050
|
legacy: true
|
|
1864
1051
|
});
|
|
1865
|
-
const removePlan = planMigration([
|
|
1866
|
-
...CONFORMANCE_USERS_SCHEMA,
|
|
1867
|
-
columns: [...CONFORMANCE_USERS_SCHEMA.columns, {
|
|
1868
|
-
name: "legacy",
|
|
1869
|
-
type: "boolean",
|
|
1870
|
-
nullable: false
|
|
1871
|
-
}]
|
|
1872
|
-
}], [CONFORMANCE_USERS_SCHEMA]);
|
|
1052
|
+
const removePlan = planMigration([deployedUsers], [CONFORMANCE_USERS_SCHEMA]);
|
|
1873
1053
|
await driver.migrate(removePlan);
|
|
1874
1054
|
const migrated = await driver.read("users", "u1");
|
|
1875
1055
|
if (migrated === void 0 || "legacy" in migrated) {
|
|
@@ -2132,173 +1312,6 @@ async function auditDriver(factory) {
|
|
|
2132
1312
|
return findings;
|
|
2133
1313
|
}
|
|
2134
1314
|
//#endregion
|
|
2135
|
-
//#region node_modules/@orkestrel/emitter/dist/src/core/index.js
|
|
2136
|
-
/**
|
|
2137
|
-
* Extract the own enumerable keys of a mapped object, typed as its key union.
|
|
2138
|
-
*
|
|
2139
|
-
* @remarks
|
|
2140
|
-
* `Object.keys` widens its result to `string[]`, which breaks the key↔value
|
|
2141
|
-
* correlation a mapped type (like `EmitterHooks<TMap>`) otherwise guarantees.
|
|
2142
|
-
* A `for…in` push into a `keyof`-typed array narrows the result back,
|
|
2143
|
-
* type-safely and with no assertion.
|
|
2144
|
-
*
|
|
2145
|
-
* @typeParam T - The object shape whose keys are extracted.
|
|
2146
|
-
* @param object - The object to read keys from.
|
|
2147
|
-
* @returns The object's own enumerable keys, typed as `(keyof T)[]`.
|
|
2148
|
-
*
|
|
2149
|
-
* @example
|
|
2150
|
-
* ```ts
|
|
2151
|
-
* import { extractKeys } from '@src/core'
|
|
2152
|
-
*
|
|
2153
|
-
* const hooks = { tick: () => {}, done: () => {} }
|
|
2154
|
-
* extractKeys(hooks) // ['tick', 'done']
|
|
2155
|
-
* extractKeys({}) // []
|
|
2156
|
-
* ```
|
|
2157
|
-
*/
|
|
2158
|
-
function extractKeys(object) {
|
|
2159
|
-
const collected = [];
|
|
2160
|
-
for (const key in object) collected.push(key);
|
|
2161
|
-
return collected;
|
|
2162
|
-
}
|
|
2163
|
-
Object.freeze([
|
|
2164
|
-
"null",
|
|
2165
|
-
"boolean",
|
|
2166
|
-
"object",
|
|
2167
|
-
"array",
|
|
2168
|
-
"number",
|
|
2169
|
-
"integer",
|
|
2170
|
-
"string"
|
|
2171
|
-
]);
|
|
2172
|
-
/** Determine whether a value is callable. */
|
|
2173
|
-
function isFunction(value) {
|
|
2174
|
-
return typeof value === "function";
|
|
2175
|
-
}
|
|
2176
|
-
/**
|
|
2177
|
-
* A typed synchronous event emitter — the foundational observable primitive of
|
|
2178
|
-
* the codebase (AGENTS §13). Stateful entities OWN one as a `#emitter` field and
|
|
2179
|
-
* expose it through `readonly emitter`; they never inherit from it.
|
|
2180
|
-
*
|
|
2181
|
-
* @typeParam TMap - The event map: each event name to the argument tuple its
|
|
2182
|
-
* listeners receive.
|
|
2183
|
-
*
|
|
2184
|
-
* @remarks
|
|
2185
|
-
* - **Synchronous.** `emit` invokes listeners in registration order, in the
|
|
2186
|
-
* current tick.
|
|
2187
|
-
* - **Listener isolation.** A throwing listener never stops its siblings: every
|
|
2188
|
-
* listener runs, and a throw is routed to the `error` handler
|
|
2189
|
-
* ({@link EmitterOptions.error}) — never rethrown. Every throwing listener
|
|
2190
|
-
* surfaces (not just the first), and with no `error` handler a throw is swallowed
|
|
2191
|
-
* silently. The `error` handler runs inside its own try/catch, so a throwing
|
|
2192
|
-
* error-handler is swallowed too (anti-recursion — it cannot escape or re-enter).
|
|
2193
|
-
* - **Per-event storage.** Listeners live in a per-event `Set`, so every public
|
|
2194
|
-
* method is precisely typed with no assertions.
|
|
2195
|
-
* - **Destroyed → no-op.** After `destroy()`, `on` / `once` / `emit` do nothing
|
|
2196
|
-
* and `destroyed` is `true`.
|
|
2197
|
-
*
|
|
2198
|
-
* @example
|
|
2199
|
-
* ```ts
|
|
2200
|
-
* type CounterEventMap = {
|
|
2201
|
-
* tick: readonly [count: number]
|
|
2202
|
-
* done: readonly []
|
|
2203
|
-
* }
|
|
2204
|
-
*
|
|
2205
|
-
* const emitter = new Emitter<CounterEventMap>({
|
|
2206
|
-
* on: { done: () => stop() },
|
|
2207
|
-
* error: (error, event) => log(`listener for ${event} threw`, error),
|
|
2208
|
-
* })
|
|
2209
|
-
* emitter.on('tick', (count) => render(count))
|
|
2210
|
-
* emitter.emit('tick', 1)
|
|
2211
|
-
* ```
|
|
2212
|
-
*/
|
|
2213
|
-
var Emitter = class {
|
|
2214
|
-
#destroyed = false;
|
|
2215
|
-
#listeners = {};
|
|
2216
|
-
#wrappers = {};
|
|
2217
|
-
#error;
|
|
2218
|
-
constructor(options) {
|
|
2219
|
-
const error = options?.error;
|
|
2220
|
-
this.#error = isFunction(error) ? error : void 0;
|
|
2221
|
-
const hooks = options?.on;
|
|
2222
|
-
if (hooks !== void 0) this.#wire(hooks);
|
|
2223
|
-
}
|
|
2224
|
-
get destroyed() {
|
|
2225
|
-
return this.#destroyed;
|
|
2226
|
-
}
|
|
2227
|
-
on(event, handler) {
|
|
2228
|
-
if (this.#destroyed) return;
|
|
2229
|
-
(this.#listeners[event] ??= /* @__PURE__ */ new Set()).add(handler);
|
|
2230
|
-
}
|
|
2231
|
-
once(event, handler) {
|
|
2232
|
-
if (this.#destroyed) return;
|
|
2233
|
-
const pending = this.#wrappers[event] ??= /* @__PURE__ */ new Map();
|
|
2234
|
-
const wrapper = (...args) => {
|
|
2235
|
-
this.#listeners[event]?.delete(wrapper);
|
|
2236
|
-
const wrappers = pending.get(handler);
|
|
2237
|
-
wrappers?.delete(wrapper);
|
|
2238
|
-
if (wrappers !== void 0 && wrappers.size === 0) pending.delete(handler);
|
|
2239
|
-
handler(...args);
|
|
2240
|
-
};
|
|
2241
|
-
const wrappers = pending.get(handler) ?? /* @__PURE__ */ new Set();
|
|
2242
|
-
wrappers.add(wrapper);
|
|
2243
|
-
pending.set(handler, wrappers);
|
|
2244
|
-
this.on(event, wrapper);
|
|
2245
|
-
}
|
|
2246
|
-
off(event, handler) {
|
|
2247
|
-
const listeners = this.#listeners[event];
|
|
2248
|
-
const wrappers = this.#wrappers[event];
|
|
2249
|
-
const pending = wrappers?.get(handler);
|
|
2250
|
-
if (pending !== void 0) {
|
|
2251
|
-
for (const wrapper of pending) listeners?.delete(wrapper);
|
|
2252
|
-
wrappers?.delete(handler);
|
|
2253
|
-
}
|
|
2254
|
-
listeners?.delete(handler);
|
|
2255
|
-
}
|
|
2256
|
-
emit(event, ...args) {
|
|
2257
|
-
if (this.#destroyed) return;
|
|
2258
|
-
const listeners = this.#listeners[event];
|
|
2259
|
-
if (listeners === void 0) return;
|
|
2260
|
-
for (const handler of [...listeners]) try {
|
|
2261
|
-
handler(...args);
|
|
2262
|
-
} catch (error) {
|
|
2263
|
-
this.#surface(error, event);
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
count(event) {
|
|
2267
|
-
if (event !== void 0) return this.#listeners[event]?.size ?? 0;
|
|
2268
|
-
let total = 0;
|
|
2269
|
-
for (const set of Object.values(this.#listeners)) total += set?.size ?? 0;
|
|
2270
|
-
return total;
|
|
2271
|
-
}
|
|
2272
|
-
clear(event) {
|
|
2273
|
-
if (event !== void 0) {
|
|
2274
|
-
delete this.#listeners[event];
|
|
2275
|
-
delete this.#wrappers[event];
|
|
2276
|
-
return;
|
|
2277
|
-
}
|
|
2278
|
-
this.#listeners = {};
|
|
2279
|
-
this.#wrappers = {};
|
|
2280
|
-
}
|
|
2281
|
-
destroy() {
|
|
2282
|
-
this.#listeners = {};
|
|
2283
|
-
this.#wrappers = {};
|
|
2284
|
-
this.#error = void 0;
|
|
2285
|
-
this.#destroyed = true;
|
|
2286
|
-
}
|
|
2287
|
-
#surface(error, event) {
|
|
2288
|
-
const handler = this.#error;
|
|
2289
|
-
if (handler === void 0) return;
|
|
2290
|
-
try {
|
|
2291
|
-
handler(error, String(event));
|
|
2292
|
-
} catch {}
|
|
2293
|
-
}
|
|
2294
|
-
#wire(hooks) {
|
|
2295
|
-
for (const event of extractKeys(hooks)) {
|
|
2296
|
-
const handler = hooks[event];
|
|
2297
|
-
if (isFunction(handler)) this.on(event, handler);
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
};
|
|
2301
|
-
//#endregion
|
|
2302
1315
|
//#region src/core/Cursor.ts
|
|
2303
1316
|
/**
|
|
2304
1317
|
* A forward row cursor for bulk in-place mutation.
|
|
@@ -2669,6 +1682,21 @@ var Table = class {
|
|
|
2669
1682
|
for (const row of source) if (this.#guard(row)) rows.push(row);
|
|
2670
1683
|
return rows;
|
|
2671
1684
|
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Count rows matching `criteria`'s conditions.
|
|
1687
|
+
*
|
|
1688
|
+
* @remarks
|
|
1689
|
+
* Unlike {@link records}, which narrows every row through the table's
|
|
1690
|
+
* contract guard before returning it, `count` operates on STORED rows
|
|
1691
|
+
* WITHOUT that guard (both the native `driver.count` hook and the
|
|
1692
|
+
* `filterRows`-over-`#collect()` fallback count raw storage) — so it can
|
|
1693
|
+
* exceed `(await records(criteria)).length` when storage holds rows that
|
|
1694
|
+
* no longer conform to the table's contract (legacy or migrated data).
|
|
1695
|
+
*
|
|
1696
|
+
* @param criteria - Optional conditions to filter by (paging is ignored)
|
|
1697
|
+
* @param options - `{ signal }` to abort
|
|
1698
|
+
* @returns The count of matching stored rows
|
|
1699
|
+
*/
|
|
2672
1700
|
async count(criteria, options) {
|
|
2673
1701
|
checkAbort(options?.signal);
|
|
2674
1702
|
await this.#ready();
|
|
@@ -2677,6 +1705,23 @@ var Table = class {
|
|
|
2677
1705
|
if (native !== void 0) return native;
|
|
2678
1706
|
return filterRows(await this.#collect(), criteria?.conditions ?? []).length;
|
|
2679
1707
|
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Compute an aggregate over `column` across rows matching `criteria`'s
|
|
1710
|
+
* conditions.
|
|
1711
|
+
*
|
|
1712
|
+
* @remarks
|
|
1713
|
+
* Like {@link count}, `aggregate` operates on STORED rows WITHOUT the
|
|
1714
|
+
* contract guard {@link records} / {@link scan} apply — a non-conforming
|
|
1715
|
+
* stored row still contributes to the computed aggregate when it matches
|
|
1716
|
+
* the conditions, even though it would never appear in `records()`'s
|
|
1717
|
+
* output.
|
|
1718
|
+
*
|
|
1719
|
+
* @param operation - The aggregate to compute
|
|
1720
|
+
* @param column - The column to aggregate
|
|
1721
|
+
* @param criteria - Optional conditions to filter by (paging is ignored)
|
|
1722
|
+
* @param options - `{ signal }` to abort
|
|
1723
|
+
* @returns The aggregate value, or `undefined` when undefined for the inputs
|
|
1724
|
+
*/
|
|
2680
1725
|
async aggregate(operation, column, criteria, options) {
|
|
2681
1726
|
checkAbort(options?.signal);
|
|
2682
1727
|
await this.#ready();
|
|
@@ -2699,7 +1744,7 @@ var Table = class {
|
|
|
2699
1744
|
*
|
|
2700
1745
|
* @param criteria - Optional conditions plus offset/limit paging
|
|
2701
1746
|
* @param options - `{ signal }` to abort mid-stream
|
|
2702
|
-
* @returns An async
|
|
1747
|
+
* @returns An async iterable of matching, guard-conforming rows
|
|
2703
1748
|
*/
|
|
2704
1749
|
async *scan(criteria, options) {
|
|
2705
1750
|
checkAbort(options?.signal);
|
|
@@ -3118,8 +2163,12 @@ var Database = class Database {
|
|
|
3118
2163
|
* @remarks
|
|
3119
2164
|
* The in-between made concrete: it runs identically in a browser or on a server,
|
|
3120
2165
|
* so it is the storage behind tests, ephemeral caches, and any code that wants
|
|
3121
|
-
* the database API without a persistent backend. Rows are copied
|
|
3122
|
-
*
|
|
2166
|
+
* the database API without a persistent backend. Rows are DEEP-copied (via
|
|
2167
|
+
* `structuredClone`) in and out — at `write`, `read`, `scan`, `stream`, and both
|
|
2168
|
+
* snapshot capture and restore — so a caller mutating a nested field of an input
|
|
2169
|
+
* row, a returned row, or a row mutated in place between snapshot and rollback
|
|
2170
|
+
* can never perturb stored state (AGENTS §11); a shallow `{ ...row }` spread
|
|
2171
|
+
* would still share nested object/array references. `snapshot`
|
|
3123
2172
|
* clones every table to give transactions an exact rollback point. `scan` and
|
|
3124
2173
|
* `keys` yield in key order — sorted by the core {@link compareValues} total
|
|
3125
2174
|
* order, the same contract the SQLite (`ORDER BY`) and IndexedDB (key-ordered
|
|
@@ -3136,10 +2185,10 @@ var MemoryDriver = class {
|
|
|
3136
2185
|
async close() {}
|
|
3137
2186
|
async read(table, key) {
|
|
3138
2187
|
const row = this.#store(table).get(key);
|
|
3139
|
-
return row === void 0 ? void 0 :
|
|
2188
|
+
return row === void 0 ? void 0 : structuredClone(row);
|
|
3140
2189
|
}
|
|
3141
2190
|
async write(table, key, row) {
|
|
3142
|
-
this.#store(table).set(key,
|
|
2191
|
+
this.#store(table).set(key, structuredClone(row));
|
|
3143
2192
|
}
|
|
3144
2193
|
async delete(table, key) {
|
|
3145
2194
|
return this.#store(table).delete(key);
|
|
@@ -3151,7 +2200,7 @@ var MemoryDriver = class {
|
|
|
3151
2200
|
const store = this.#store(table);
|
|
3152
2201
|
for (const key of this.#ordered(table)) {
|
|
3153
2202
|
const row = store.get(key);
|
|
3154
|
-
if (row !== void 0) yield
|
|
2203
|
+
if (row !== void 0) yield structuredClone(row);
|
|
3155
2204
|
}
|
|
3156
2205
|
}
|
|
3157
2206
|
/**
|
|
@@ -3195,7 +2244,7 @@ var MemoryDriver = class {
|
|
|
3195
2244
|
skipped += 1;
|
|
3196
2245
|
continue;
|
|
3197
2246
|
}
|
|
3198
|
-
yield
|
|
2247
|
+
yield structuredClone(row);
|
|
3199
2248
|
yielded += 1;
|
|
3200
2249
|
}
|
|
3201
2250
|
}
|
|
@@ -3219,12 +2268,16 @@ var MemoryDriver = class {
|
|
|
3219
2268
|
const copy = /* @__PURE__ */ new Map();
|
|
3220
2269
|
for (const [name, store] of this.#tables) {
|
|
3221
2270
|
const cloned = /* @__PURE__ */ new Map();
|
|
3222
|
-
for (const [key, row] of store) cloned.set(key,
|
|
2271
|
+
for (const [key, row] of store) cloned.set(key, structuredClone(row));
|
|
3223
2272
|
copy.set(name, cloned);
|
|
3224
2273
|
}
|
|
3225
2274
|
return async () => {
|
|
3226
2275
|
this.#tables.clear();
|
|
3227
|
-
for (const [name, store] of copy)
|
|
2276
|
+
for (const [name, store] of copy) {
|
|
2277
|
+
const restored = /* @__PURE__ */ new Map();
|
|
2278
|
+
for (const [key, row] of store) restored.set(key, structuredClone(row));
|
|
2279
|
+
this.#tables.set(name, restored);
|
|
2280
|
+
}
|
|
3228
2281
|
};
|
|
3229
2282
|
}
|
|
3230
2283
|
const copy = /* @__PURE__ */ new Map();
|
|
@@ -3232,11 +2285,15 @@ var MemoryDriver = class {
|
|
|
3232
2285
|
const store = this.#tables.get(name);
|
|
3233
2286
|
if (store === void 0) continue;
|
|
3234
2287
|
const cloned = /* @__PURE__ */ new Map();
|
|
3235
|
-
for (const [key, row] of store) cloned.set(key,
|
|
2288
|
+
for (const [key, row] of store) cloned.set(key, structuredClone(row));
|
|
3236
2289
|
copy.set(name, cloned);
|
|
3237
2290
|
}
|
|
3238
2291
|
return async () => {
|
|
3239
|
-
for (const [name, store] of copy)
|
|
2292
|
+
for (const [name, store] of copy) {
|
|
2293
|
+
const restored = /* @__PURE__ */ new Map();
|
|
2294
|
+
for (const [key, row] of store) restored.set(key, structuredClone(row));
|
|
2295
|
+
this.#tables.set(name, restored);
|
|
2296
|
+
}
|
|
3240
2297
|
};
|
|
3241
2298
|
}
|
|
3242
2299
|
/**
|
|
@@ -3359,6 +2416,6 @@ function createMemoryDriver() {
|
|
|
3359
2416
|
return new MemoryDriver();
|
|
3360
2417
|
}
|
|
3361
2418
|
//#endregion
|
|
3362
|
-
export { Clause, Cursor, DEFAULT_PRIMARY, Database, DatabaseError, MAX_PATTERN_LENGTH, MemoryDriver, Query, Table, applyCriteria, auditDriver, checkAbort, compareValues, computeAggregate, conformDriver, createDatabase, createMemoryDriver, deepEqual, driverFindings, extractKey, filterRows, globMatch, isDatabaseError, likeMatch, matchesCondition, matchesCriteria, migrateRows, planMigration, shapeToColumnType, sortRows, wildcardMatch };
|
|
2419
|
+
export { Clause, Cursor, DEFAULT_PRIMARY, Database, DatabaseError, MAX_PATTERN_LENGTH, MemoryDriver, Query, Table, applyCriteria, auditDriver, checkAbort, compareValues, computeAggregate, conformDriver, createDatabase, createMemoryDriver, deepEqual, driverFindings, extractKey, filterRows, globMatch, isDatabaseError, isDriverMeta, likeMatch, matchesCondition, matchesCriteria, migrateRows, planMigration, shapeToColumnType, sortRows, wildcardMatch };
|
|
3363
2420
|
|
|
3364
2421
|
//# sourceMappingURL=index.js.map
|