@orkestrel/database 0.0.13 → 0.0.14

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.
@@ -2,7 +2,7 @@ import { cloneJSONRecord, cloneJSONValue, compileGuard, compileSchema, createCon
2
2
  import { Emitter } from "@orkestrel/emitter";
3
3
  //#region src/core/constants.ts
4
4
  /**
5
- * Supplies the primary-key column assumed when {@link PrimaryMap} does not name one.
5
+ * Supplies the primary-key column, `'id'`, assumed when {@link PrimaryMap} does not name one.
6
6
  *
7
7
  * @remarks
8
8
  * `id` is the convention IndexedDB (`keyPath: 'id'`) and SQL (`id` / rowid) both
@@ -10,12 +10,13 @@ import { Emitter } from "@orkestrel/emitter";
10
10
  */
11
11
  var DEFAULT_PRIMARY = "id";
12
12
  /**
13
- * Sets the longest `LIKE` / `GLOB` pattern the wildcard matcher accepts before rejecting it.
13
+ * Sets the longest `LIKE` / `GLOB` pattern the wildcard matcher accepts, 1024 characters, before
14
+ * rejecting it.
14
15
  *
15
16
  * @remarks
16
17
  * A `LIKE` / `GLOB` pattern is a caller-supplied operand, so
17
18
  * `matchesLikePattern` / `matchesGlobPattern` run patterns this package cannot
18
- * trust. The matcher is the LINEAR greedy two-pointer wildcard match — never a
19
+ * trust. The matcher is the linear greedy two-pointer wildcard match — never a
19
20
  * backtracking regex (`.*`-segments-separated-by-literals against a long input is the
20
21
  * catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
21
22
  * pattern). Capping the pattern length bounds that pattern factor, leaving a match
@@ -89,11 +90,13 @@ var CONFORMANCE_POSTS_SCHEMA = Object.freeze({
89
90
  indexes: Object.freeze([])
90
91
  });
91
92
  /**
92
- * Holds the fixed two-table schema every driver-conformance phase opens.
93
+ * Holds the fixed `users` and `posts` schema every driver-conformance phase opens.
93
94
  *
94
95
  * @remarks
95
96
  * Each phase mints a fresh driver and opens this exact schema, so a finding
96
- * names a violated invariant rather than a setup difference between phases.
97
+ * names a violated invariant rather than a setup difference between phases. The
98
+ * array and each schema in it are frozen, so a consumer holding it cannot change
99
+ * what a later phase opens.
97
100
  */
98
101
  var CONFORMANCE_SCHEMA = Object.freeze([CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA]);
99
102
  //#endregion
@@ -156,6 +159,10 @@ function isKey(value) {
156
159
  /**
157
160
  * Checks whether a value is a portable column schema.
158
161
  *
162
+ * @remarks
163
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
164
+ * contained as a non-match rather than a throw.
165
+ *
159
166
  * @param value - The value to test
160
167
  * @returns True if `value` is a complete {@link ColumnSchema}; false otherwise
161
168
  */
@@ -171,6 +178,10 @@ function isColumnSchema(value) {
171
178
  /**
172
179
  * Checks whether a value is a portable table schema.
173
180
  *
181
+ * @remarks
182
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
183
+ * contained as a non-match rather than a throw.
184
+ *
174
185
  * @param value - The value to test
175
186
  * @returns True if `value` is a complete {@link TableSchema}; false otherwise
176
187
  */
@@ -190,6 +201,10 @@ function isTableSchema(value) {
190
201
  /**
191
202
  * Checks whether a value is a complete portable driver schema.
192
203
  *
204
+ * @remarks
205
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
206
+ * contained as a non-match rather than a throw.
207
+ *
193
208
  * @param value - The value to test
194
209
  * @returns True if `value` is a table-schema collection with unique table names; false otherwise
195
210
  */
@@ -206,6 +221,10 @@ function isDriverSchema(value) {
206
221
  /**
207
222
  * Checks whether a value is one ordered migration step.
208
223
  *
224
+ * @remarks
225
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
226
+ * contained as a non-match rather than a throw.
227
+ *
209
228
  * @param value - The value to test
210
229
  * @returns True if `value` is a complete {@link MigrationStep}; false otherwise
211
230
  */
@@ -230,6 +249,10 @@ function isMigrationStep(value) {
230
249
  /**
231
250
  * Checks whether a value is an ordered migration plan.
232
251
  *
252
+ * @remarks
253
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
254
+ * contained as a non-match rather than a throw.
255
+ *
233
256
  * @param value - The value to test
234
257
  * @returns True if `value` is a complete {@link Migration}; false otherwise
235
258
  */
@@ -245,6 +268,12 @@ function isMigration(value) {
245
268
  /**
246
269
  * Checks whether a value is persisted driver metadata.
247
270
  *
271
+ * @remarks
272
+ * The boundary check a versioning driver's `metadata()` narrows a stored or
273
+ * deserialized record through, so no call site needs an assertion. Total over any
274
+ * input: a hostile getter, a revoked proxy, or a cyclic value is contained as a
275
+ * non-match rather than a throw.
276
+ *
248
277
  * @param value - The value to test
249
278
  * @returns True if `value` is complete {@link DriverMetadata}; false otherwise
250
279
  */
@@ -260,6 +289,10 @@ function isDriverMetadata(value) {
260
289
  /**
261
290
  * Checks whether a value is one atomic migration request.
262
291
  *
292
+ * @remarks
293
+ * Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
294
+ * contained as a non-match rather than a throw.
295
+ *
263
296
  * @param value - The value to test
264
297
  * @returns True if `value` is a complete {@link MigrationInput}; false otherwise
265
298
  */
@@ -277,6 +310,11 @@ function isMigrationInput(value) {
277
310
  /**
278
311
  * Clones unknown driver metadata into a distinct deeply frozen snapshot.
279
312
  *
313
+ * @remarks
314
+ * The clone is validated as {@link DriverMetadata} before it is returned, so a
315
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
316
+ * `context.path === 'metadata'` rather than surfacing a raw Contract or caller error.
317
+ *
280
318
  * @param value - Unknown metadata
281
319
  * @returns Owned driver metadata
282
320
  */
@@ -296,6 +334,11 @@ function cloneDriverMetadata(value) {
296
334
  /**
297
335
  * Clones unknown driver schema into a distinct deeply frozen snapshot.
298
336
  *
337
+ * @remarks
338
+ * The clone is validated as a table-schema collection before it is returned, so a
339
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
340
+ * `context.path === 'schema'` rather than surfacing a raw Contract or caller error.
341
+ *
299
342
  * @param value - Unknown table schema collection
300
343
  * @returns Owned driver schema
301
344
  */
@@ -315,6 +358,11 @@ function cloneDriverSchema(value) {
315
358
  /**
316
359
  * Clones unknown migration input into a distinct deeply frozen snapshot.
317
360
  *
361
+ * @remarks
362
+ * The clone is validated as a {@link MigrationInput} before it is returned, so a
363
+ * malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
364
+ * `context.path === 'migration'` rather than surfacing a raw Contract or caller error.
365
+ *
318
366
  * @param value - Unknown migration input
319
367
  * @returns Owned migration input
320
368
  */
@@ -390,9 +438,9 @@ function compareValues(left, right) {
390
438
  * @remarks
391
439
  * Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
392
440
  * Arrays compare by index (same length, every element `equalsValue`). Plain
393
- * records (through `isRecord`) compare by their OWN enumerable keys: same key
394
- * COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
395
- * with a `equalsValue` value — so a key present with value `undefined` is NOT
441
+ * records (through `isRecord`) compare by their own enumerable keys: same key
442
+ * count and, for every key in `left`, `right` has that key (`Object.hasOwn`)
443
+ * with a `equalsValue` value — so a key present with value `undefined` is not
396
444
  * equal to that key being absent (both differ in `Object.keys` membership).
397
445
  * Anything else (functions, class instances, mismatched shapes) falls through
398
446
  * to `false`. Container pairs are tracked iteratively, so self-referential and
@@ -458,16 +506,16 @@ function equalsValue(left, right) {
458
506
  }
459
507
  }
460
508
  /**
461
- * Matches a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
509
+ * Matches a value against a wildcard pattern in linear time — the shared, ReDoS-safe
462
510
  * engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
463
511
  *
464
512
  * @remarks
465
- * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
513
+ * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is catastrophic on a hostile pattern:
466
514
  * `.*` segments separated by literals, matched against a long non-matching input, blow
467
515
  * up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it,
468
516
  * while a `LIKE` / `GLOB` pattern is a caller-supplied operand this package cannot
469
- * trust. So this builds NO regex. It runs the classic GREEDY TWO-POINTER wildcard match:
470
- * the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
517
+ * trust. So this builds no regex. It runs the classic greedy two-pointer wildcard match:
518
+ * the `any` wildcard records its position and, on a later mismatch, backtracks only to
471
519
  * that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
472
520
  * never the exponential / polynomial backtracking a regex would do. The pattern length
473
521
  * is capped at {@link MAX_PATTERN_LENGTH} (a `VALIDATION` {@link DatabaseError} over it),
@@ -475,16 +523,16 @@ function equalsValue(left, right) {
475
523
  * pattern.
476
524
  *
477
525
  * The `any` wildcard matches any run (including empty); `single` matches exactly one
478
- * char; every other pattern char matches itself LITERALLY (a pattern `.` / `(` / `\` is
526
+ * char; every other pattern char matches itself literally (a pattern `.` / `(` / `\` is
479
527
  * a literal — the regex-metacharacter hazard is gone with the regex). `any` is tested
480
- * BEFORE a literal match, so a value that literally contains the wildcard char never
481
- * shadows the wildcard. Case folding is applied to BOTH sides when `fold` is set.
528
+ * before a literal match, so a value that literally contains the wildcard char never
529
+ * shadows the wildcard. Case folding is applied to both sides when `fold` is set.
482
530
  *
483
531
  * @param value - The value to test
484
532
  * @param pattern - The wildcard pattern
485
533
  * @param any - The any-run wildcard char (`%` for `LIKE`, `*` for `GLOB`)
486
534
  * @param single - The single-char wildcard char (`_` for `LIKE`, `?` for `GLOB`)
487
- * @param fold - Whether to match case-INSENSITIVELY (`LIKE` folds; `GLOB` does not)
535
+ * @param fold - Whether to match case-insensitively (`LIKE` folds; `GLOB` does not)
488
536
  * @returns True if `value` matches `pattern`; false otherwise
489
537
  * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
490
538
  */
@@ -546,7 +594,7 @@ function matchesLikePattern(value, pattern) {
546
594
  * @remarks
547
595
  * `*` matches any run of characters (including none) and `?` matches exactly one
548
596
  * character; every other pattern character matches itself literally, so a
549
- * character class such as `[a-z]` is NOT interpreted. Runs on
597
+ * character class such as `[a-z]` is not interpreted. Runs on
550
598
  * {@link matchesWildcardPattern}, so the match is linear in the value length and
551
599
  * the pattern is capped at {@link MAX_PATTERN_LENGTH}.
552
600
  *
@@ -572,7 +620,7 @@ function matchesGlobPattern(value, pattern) {
572
620
  * string is one column; an array descends a nested value) — and applies the
573
621
  * operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
574
622
  * {@link compareValues}, the total order; the equality family (`equals` / `not`
575
- * / `any` / `none`) uses {@link equalsValue} — STRUCTURAL equality, not the total
623
+ * / `any` / `none`) uses {@link equalsValue} — structural equality, not the total
576
624
  * order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
577
625
  * operand only matches a structurally-equal value, never every row holding any
578
626
  * object. This is a semantics change from ranking: `equalsValue` is SameValueZero
@@ -730,7 +778,9 @@ function computeAggregate(rows, operation, column) {
730
778
  const total = numbers.reduce((sum, value) => sum + value, 0);
731
779
  return operation === "average" ? total / numbers.length : total;
732
780
  }
733
- return operation === "minimum" ? Math.min(...numbers) : Math.max(...numbers);
781
+ let result = operation === "minimum" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
782
+ for (const value of numbers) result = operation === "minimum" ? Math.min(result, value) : Math.max(result, value);
783
+ return result;
734
784
  }
735
785
  /**
736
786
  * Reads a row's primary key from a column, when it is a usable {@link Key}.
@@ -908,16 +958,16 @@ function checkAbort(signal) {
908
958
  * plan labels only; versioning drivers persist and reconcile them through
909
959
  * {@link DriverMetadata}.
910
960
  *
911
- * A column present in BOTH schemas under the same name but with a different
961
+ * A column present in both schemas under the same name but with a different
912
962
  * `storage`, `optional`, or `nullable` value throws a `MIGRATION`
913
963
  * {@link DatabaseError} naming the table, the column, and the from→to
914
- * difference — a name-only diff would otherwise silently produce NO step for
964
+ * difference — a name-only diff would otherwise silently produce no step for
915
965
  * the drift, and versioned reconciliation would stamp over it. There is no
916
966
  * automatic in-place type-change step: the manual path is to add a new column,
917
967
  * copy/convert the data at the application layer, then remove the old column —
918
968
  * two separate plans, never a single implicit "alter" step.
919
969
  *
920
- * @param deployed - The table schemas currently applied
970
+ * @param deployed - The already-applied table schemas
921
971
  * @param declared - The table schemas the caller wants applied
922
972
  * @param from - The plan's source version label (defaults to `0`)
923
973
  * @param to - The plan's target version label (defaults to `1`)
@@ -1024,6 +1074,8 @@ function planMigration(deployed, declared, from = 0, to = 1) {
1024
1074
  }
1025
1075
  /**
1026
1076
  * Projects migration steps sequentially over a canonical validated owned schema.
1077
+ *
1078
+ * @remarks
1027
1079
  * Adding a required non-null column to an existing table rejects with
1028
1080
  * `MIGRATION`; optional-only and nullable-only additions remain portable.
1029
1081
  *
@@ -1187,14 +1239,14 @@ function migrateRows(rows, steps) {
1187
1239
  * default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
1188
1240
  * fresh for each phase so failures stay isolated, verifies: `open`/`close`;
1189
1241
  * `read` of a missing key returns `undefined`; `write`/`read` round-trip
1190
- * with DEEP copy-in/copy-out isolation (mutating the caller's row —
1191
- * including a NESTED field — after `write`, or a row `read` returns, never
1242
+ * with deep copy-in/copy-out isolation (mutating the caller's row —
1243
+ * including a nested field — after `write`, or a row `read` returns, never
1192
1244
  * perturbs stored state) and upsert-overwrite; simultaneous same-key
1193
1245
  * `insert` calls produce exactly one commit and one `CONFLICT`; pre-aborted
1194
1246
  * `write`, `insert`, and `delete` calls leave storage unchanged; `delete`
1195
1247
  * returns `true` then `false`; `keys`/`scan` yield in ascending key order;
1196
1248
  * `clear` empties only its target table; `snapshot`'s rollback thunk
1197
- * restores pre-snapshot state, including a NESTED field mutated in place on
1249
+ * restores pre-snapshot state, including a nested field mutated in place on
1198
1250
  * a read-back row between capture and restore; a scoped
1199
1251
  * `snapshot(['users'])` rolls back only the named table, leaving a
1200
1252
  * concurrent mutation to another table intact; a non-`id` primary key
@@ -1208,12 +1260,12 @@ function migrateRows(rows, steps) {
1208
1260
  * store's `metadata()` is `undefined`, and after
1209
1261
  * `stamp({ version, schema })`, `metadata()` returns the exact stamped value.
1210
1262
  *
1211
- * Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
1212
- * finding built from the assertion, while an UNEXPECTED throw (a driver
1263
+ * Each phase runs within a `try`/`catch`: an expected mismatch yields a
1264
+ * finding built from the assertion, while an unexpected throw (a driver
1213
1265
  * crash mid-phase) is caught and yielded as a finding too, naming the phase
1214
1266
  * as `check` and carrying the caught error in `context.error` — a broken
1215
1267
  * driver can never escape the battery as an unhandled rejection. Within a
1216
- * phase, the FIRST violated assertion yields and the phase stops (matching
1268
+ * phase, the first violated assertion yields and the phase stops (matching
1217
1269
  * the historical fail-fast shape at phase granularity); the generator then
1218
1270
  * moves on to the next phase regardless. Because this is a **generator**,
1219
1271
  * consuming only the first yielded value reproduces true fail-fast (later
@@ -2024,11 +2076,13 @@ async function* scanDriver(factory) {
2024
2076
  *
2025
2077
  * @remarks
2026
2078
  * Consumes only the first value {@link scanDriver} yields: because that
2027
- * generator is lazy, every LATER phase never runs — true fail-fast, not
2079
+ * generator is lazy, every later phase never runs — true fail-fast, not
2028
2080
  * merely "report only the first". The
2029
2081
  * thrown error is byte-compatible with the historical shape: a
2030
2082
  * `CONFORMANCE` {@link DatabaseError} whose `message` is the finding's
2031
- * `message` and whose `context` is `{ check, ...finding.context }`.
2083
+ * `message` and whose `context` is `{ check, ...finding.context }`. The battery
2084
+ * takes a driver factory and reports through a throw, so it binds no test
2085
+ * framework and runs from any runner.
2032
2086
  *
2033
2087
  * @param factory - Mints a fresh, unopened driver instance (called once per phase)
2034
2088
  * @returns Nothing — resolves once every phase has passed
@@ -2048,7 +2102,7 @@ async function conformDriver(factory) {
2048
2102
  });
2049
2103
  }
2050
2104
  /**
2051
- * Runs the FULL driver-conformance battery and collects every violation — the
2105
+ * Runs the full driver-conformance battery and collects every violation — the
2052
2106
  * audit entry point for a driver author who wants a complete report rather
2053
2107
  * than a single fail-fast throw.
2054
2108
  *
@@ -2718,10 +2772,10 @@ var Query = class {
2718
2772
  * @remarks
2719
2773
  * - **Observable.** The owned {@link emitter} ({@link TableEventMap}) carries the
2720
2774
  * per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
2721
- * fire-and-forget observers (cache invalidation, sync, an audit log), ALONGSIDE the
2722
- * database-level lifecycle. Events carry the affected KEY only (no value payload, to
2775
+ * fire-and-forget observers (cache invalidation, sync, an audit log), alongside the
2776
+ * database-level lifecycle. Events carry the affected key only (no value payload, to
2723
2777
  * keep fan-out lean); reads / queries / counts are not emitted. Every event is emitted
2724
- * directly, strictly AFTER the driver write / delete / clear completes; the emitter
2778
+ * directly, strictly after the driver write / delete / clear completes; the emitter
2725
2779
  * isolates a listener throw and routes it to its `error` handler (the `error` option),
2726
2780
  * so a buggy observer can never corrupt a write or perturb a transaction.
2727
2781
  */
@@ -2834,7 +2888,7 @@ var Table = class {
2834
2888
  * conditions.
2835
2889
  *
2836
2890
  * @remarks
2837
- * Unlike {@link count}, `aggregate` operates on STORED rows WITHOUT the
2891
+ * Unlike {@link count}, `aggregate` operates on stored rows without the
2838
2892
  * contract guard {@link records} / {@link scan} apply — a non-conforming
2839
2893
  * stored row still contributes to the computed aggregate when it matches
2840
2894
  * the conditions, even though it would never appear in `records()`'s
@@ -3162,6 +3216,10 @@ var DatabaseTransaction = class {
3162
3216
  * generator. Imported views register their physical schemas with the same
3163
3217
  * internal context before opening begins, so every view observes one driver,
3164
3218
  * merged schema, emitter, status, transaction boundary, and terminal close.
3219
+ *
3220
+ * The view owns the driver and its declared `tables`, connects that driver lazily on
3221
+ * first use, `import`s further tables and `export`s their portable definitions, and
3222
+ * runs `transaction` scopes over the shared context.
3165
3223
  */
3166
3224
  var Database = class Database {
3167
3225
  #context;
@@ -3284,7 +3342,7 @@ var Database = class Database {
3284
3342
  * @remarks
3285
3343
  * The in-between made concrete: it runs identically in a browser or on a server,
3286
3344
  * so it is the storage behind tests, ephemeral caches, and any code that wants
3287
- * the database API without a persistent backend. Rows are DEEP-copied (through
3345
+ * the database API without a persistent backend. Rows are deep-copied (through
3288
3346
  * `structuredClone`) in and out — at `write`, `read`, `scan`, `stream`, and both
3289
3347
  * snapshot capture and restore — so a caller mutating a nested field of an input
3290
3348
  * row, a returned row, or a row mutated in place between snapshot and rollback
@@ -3359,7 +3417,7 @@ var MemoryDriver = class {
3359
3417
  * toward `offset` / `limit`. Both are applied lazily as matches are found —
3360
3418
  * `offset` matches are skipped without being yielded, and iteration stops the
3361
3419
  * instant `limit` yields have been produced, so a large table is never fully
3362
- * walked for a small page. `input.order` is IGNORED (the same contract as
3420
+ * walked for a small page. `input.order` is ignored (the same contract as
3363
3421
  * `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
3364
3422
  * order, sorted output is `records()`'s job. Rows yield copy-out, and an
3365
3423
  * unknown table mirrors `scan`'s empty-yield behavior.
@@ -3588,20 +3646,29 @@ var MemoryDriver = class {
3588
3646
  * `name`, `generator`, `version`, and emitter hooks
3589
3647
  * @returns A typed {@link DatabaseInterface}
3590
3648
  *
3591
- * @example
3649
+ * @example Create a database
3592
3650
  * ```ts
3593
3651
  * import { createDatabase, createMemoryDriver } from '@orkestrel/database'
3594
3652
  * import { integerShape, stringShape } from '@orkestrel/contract'
3595
3653
  *
3596
3654
  * const db = createDatabase({
3597
- * driver: createMemoryDriver(),
3655
+ * driver: createMemoryDriver(), // any DriverInterface — a persistent backend swaps in, same API
3598
3656
  * tables: {
3599
- * users: { id: stringShape(), age: integerShape() },
3657
+ * users: { id: stringShape(), name: stringShape(), age: integerShape() },
3600
3658
  * posts: { slug: stringShape(), title: stringShape() },
3601
3659
  * },
3602
- * primary: { posts: 'slug' },
3660
+ * primary: { posts: 'slug' }, // non-`id` primary-key columns, per table
3603
3661
  * })
3604
- * await db.table('users').set({ id: 'u1', age: 36 }) // typed; coerced + validated
3662
+ *
3663
+ * const users = db.table('users') // hold the handle; TableInterface<{ id; name; age }>
3664
+ *
3665
+ * await users.set({ id: 'u1', name: 'Ada', age: 36 }) // coerced + validated through the contract
3666
+ * await users.get('u1') // typed { id; name; age } | undefined — narrowed, never `as`
3667
+ * await users
3668
+ * .query()
3669
+ * .condition({ column: 'age', operator: 'from', values: [18], connector: 'and' })
3670
+ * .order({ column: 'age', direction: 'descending' })
3671
+ * .collect() // typed rows
3605
3672
  * ```
3606
3673
  */
3607
3674
  function createDatabase(options) {