@orkestrel/database 0.0.13 → 0.0.15
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 +10 -6
- package/dist/src/browser/index.d.ts +54 -51
- package/dist/src/browser/index.js +37 -31
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +170 -117
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +274 -61
- package/dist/src/core/index.d.ts +274 -61
- package/dist/src/core/index.js +171 -118
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +78 -58
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +93 -76
- package/dist/src/server/index.d.ts +93 -76
- package/dist/src/server/index.js +79 -59
- package/dist/src/server/index.js.map +1 -1
- package/package.json +17 -18
package/dist/src/core/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { cloneJSONRecord, cloneJSONValue, compileGuard, compileSchema, createContract, isArray, isRecord, isString, objectShape, parseNumber, resolveField } from "@orkestrel/contract";
|
|
1
|
+
import { arrayOf, cloneJSONRecord, cloneJSONValue, compileGuard, compileSchema, createContract, holds, isArray, isBoolean, isError, isFiniteNumber, isInstance, isInteger, isNumber, isRecord, isString, objectShape, parseNumber, resolveField } from "@orkestrel/contract";
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -140,7 +143,7 @@ var DatabaseError = class extends Error {
|
|
|
140
143
|
* ```
|
|
141
144
|
*/
|
|
142
145
|
function isDatabaseError(value) {
|
|
143
|
-
return value
|
|
146
|
+
return isInstance(value, DatabaseError);
|
|
144
147
|
}
|
|
145
148
|
//#endregion
|
|
146
149
|
//#region src/core/validators.ts
|
|
@@ -151,132 +154,153 @@ function isDatabaseError(value) {
|
|
|
151
154
|
* @returns True if `value` is a string or a finite number; false otherwise
|
|
152
155
|
*/
|
|
153
156
|
function isKey(value) {
|
|
154
|
-
return
|
|
157
|
+
return isString(value) || isFiniteNumber(value);
|
|
155
158
|
}
|
|
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
|
*/
|
|
162
169
|
function isColumnSchema(value) {
|
|
163
|
-
|
|
170
|
+
return holds(() => {
|
|
164
171
|
const column = cloneJSONRecord(value);
|
|
165
172
|
const keys = Object.keys(column);
|
|
166
|
-
return keys.length === 4 && keys.includes("name") && keys.includes("storage") && keys.includes("optional") && keys.includes("nullable") &&
|
|
167
|
-
}
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
173
|
+
return keys.length === 4 && keys.includes("name") && keys.includes("storage") && keys.includes("optional") && keys.includes("nullable") && isString(column.name) && column.name.length > 0 && (column.storage === "text" || column.storage === "integer" || column.storage === "real" || column.storage === "boolean" || column.storage === "json" || column.storage === "blob") && isBoolean(column.optional) && isBoolean(column.nullable);
|
|
174
|
+
});
|
|
170
175
|
}
|
|
171
176
|
/**
|
|
172
177
|
* Checks whether a value is a portable table schema.
|
|
173
178
|
*
|
|
179
|
+
* @remarks
|
|
180
|
+
* Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
|
|
181
|
+
* contained as a non-match rather than a throw.
|
|
182
|
+
*
|
|
174
183
|
* @param value - The value to test
|
|
175
184
|
* @returns True if `value` is a complete {@link TableSchema}; false otherwise
|
|
176
185
|
*/
|
|
177
186
|
function isTableSchema(value) {
|
|
178
|
-
|
|
187
|
+
return holds(() => {
|
|
179
188
|
const table = cloneJSONRecord(value);
|
|
180
189
|
const keys = Object.keys(table);
|
|
181
|
-
if (keys.length !== 4 || !keys.includes("name") || !keys.includes("primary") || !keys.includes("columns") || !keys.includes("indexes") ||
|
|
190
|
+
if (keys.length !== 4 || !keys.includes("name") || !keys.includes("primary") || !keys.includes("columns") || !keys.includes("indexes") || !isString(table.name) || table.name.length === 0 || !isString(table.primary) || table.primary.length === 0 || !arrayOf(isColumnSchema)(table.columns) || !isArray(table.indexes)) return false;
|
|
182
191
|
const names = table.columns.map((column) => column.name);
|
|
183
|
-
if (new Set(names).size !== names.length || !names.includes(table.primary) || !table.indexes.every((index) =>
|
|
192
|
+
if (new Set(names).size !== names.length || !names.includes(table.primary) || !table.indexes.every((index) => isArray(index) && index.length > 0 && index.every((column) => isString(column) && names.includes(column)))) return false;
|
|
184
193
|
const indexes = table.indexes.map((index) => JSON.stringify(index));
|
|
185
194
|
return new Set(indexes).size === indexes.length;
|
|
186
|
-
}
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
195
|
+
});
|
|
189
196
|
}
|
|
190
197
|
/**
|
|
191
198
|
* Checks whether a value is a complete portable driver schema.
|
|
192
199
|
*
|
|
200
|
+
* @remarks
|
|
201
|
+
* Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
|
|
202
|
+
* contained as a non-match rather than a throw.
|
|
203
|
+
*
|
|
193
204
|
* @param value - The value to test
|
|
194
205
|
* @returns True if `value` is a table-schema collection with unique table names; false otherwise
|
|
195
206
|
*/
|
|
196
207
|
function isDriverSchema(value) {
|
|
197
|
-
|
|
208
|
+
return holds(() => {
|
|
198
209
|
const schema = cloneJSONValue(value);
|
|
199
|
-
if (!
|
|
210
|
+
if (!arrayOf(isTableSchema)(schema)) return false;
|
|
200
211
|
const names = schema.map((table) => table.name);
|
|
201
212
|
return new Set(names).size === names.length;
|
|
202
|
-
}
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
213
|
+
});
|
|
205
214
|
}
|
|
206
215
|
/**
|
|
207
216
|
* Checks whether a value is one ordered migration step.
|
|
208
217
|
*
|
|
218
|
+
* @remarks
|
|
219
|
+
* Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
|
|
220
|
+
* contained as a non-match rather than a throw.
|
|
221
|
+
*
|
|
209
222
|
* @param value - The value to test
|
|
210
223
|
* @returns True if `value` is a complete {@link MigrationStep}; false otherwise
|
|
211
224
|
*/
|
|
212
225
|
function isMigrationStep(value) {
|
|
213
|
-
|
|
226
|
+
return holds(() => {
|
|
214
227
|
const step = cloneJSONRecord(value);
|
|
215
|
-
if (
|
|
228
|
+
if (!isString(step.operation)) return false;
|
|
216
229
|
const keys = Object.keys(step);
|
|
217
230
|
switch (step.operation) {
|
|
218
231
|
case "table.add": return keys.length === 2 && keys.includes("operation") && keys.includes("table") && isTableSchema(step.table);
|
|
219
|
-
case "table.remove": return keys.length === 2 && keys.includes("operation") && keys.includes("table") &&
|
|
220
|
-
case "column.add": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") &&
|
|
221
|
-
case "column.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") &&
|
|
232
|
+
case "table.remove": return keys.length === 2 && keys.includes("operation") && keys.includes("table") && isString(step.table) && step.table.length > 0;
|
|
233
|
+
case "column.add": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") && isString(step.table) && step.table.length > 0 && isColumnSchema(step.column);
|
|
234
|
+
case "column.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") && isString(step.table) && step.table.length > 0 && isString(step.column) && step.column.length > 0;
|
|
222
235
|
case "index.add":
|
|
223
|
-
case "index.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("index") &&
|
|
236
|
+
case "index.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("index") && isString(step.table) && step.table.length > 0 && isArray(step.index) && step.index.length > 0 && step.index.every((column) => isString(column) && column.length > 0);
|
|
224
237
|
default: return false;
|
|
225
238
|
}
|
|
226
|
-
}
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
239
|
+
});
|
|
229
240
|
}
|
|
230
241
|
/**
|
|
231
242
|
* Checks whether a value is an ordered migration plan.
|
|
232
243
|
*
|
|
244
|
+
* @remarks
|
|
245
|
+
* Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
|
|
246
|
+
* contained as a non-match rather than a throw.
|
|
247
|
+
*
|
|
233
248
|
* @param value - The value to test
|
|
234
249
|
* @returns True if `value` is a complete {@link Migration}; false otherwise
|
|
235
250
|
*/
|
|
236
251
|
function isMigration(value) {
|
|
237
|
-
|
|
252
|
+
return holds(() => {
|
|
238
253
|
const migration = cloneJSONRecord(value);
|
|
239
254
|
const keys = Object.keys(migration);
|
|
240
|
-
return keys.length === 3 && keys.includes("from") && keys.includes("to") && keys.includes("steps") &&
|
|
241
|
-
}
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
255
|
+
return keys.length === 3 && keys.includes("from") && keys.includes("to") && keys.includes("steps") && isFiniteNumber(migration.from) && isFiniteNumber(migration.to) && isArray(migration.steps) && migration.steps.every(isMigrationStep);
|
|
256
|
+
});
|
|
244
257
|
}
|
|
245
258
|
/**
|
|
246
259
|
* Checks whether a value is persisted driver metadata.
|
|
247
260
|
*
|
|
261
|
+
* @remarks
|
|
262
|
+
* The boundary check a versioning driver's `metadata()` narrows a stored or
|
|
263
|
+
* deserialized record through, so no call site needs an assertion. Total over any
|
|
264
|
+
* input: a hostile getter, a revoked proxy, or a cyclic value is contained as a
|
|
265
|
+
* non-match rather than a throw.
|
|
266
|
+
*
|
|
248
267
|
* @param value - The value to test
|
|
249
268
|
* @returns True if `value` is complete {@link DriverMetadata}; false otherwise
|
|
250
269
|
*/
|
|
251
270
|
function isDriverMetadata(value) {
|
|
252
|
-
|
|
271
|
+
return holds(() => {
|
|
253
272
|
const metadata = cloneJSONRecord(value);
|
|
254
273
|
const keys = Object.keys(metadata);
|
|
255
|
-
return keys.length === 2 && keys.includes("version") && keys.includes("schema") &&
|
|
256
|
-
}
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
274
|
+
return keys.length === 2 && keys.includes("version") && keys.includes("schema") && isFiniteNumber(metadata.version) && isDriverSchema(metadata.schema);
|
|
275
|
+
});
|
|
259
276
|
}
|
|
260
277
|
/**
|
|
261
278
|
* Checks whether a value is one atomic migration request.
|
|
262
279
|
*
|
|
280
|
+
* @remarks
|
|
281
|
+
* Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
|
|
282
|
+
* contained as a non-match rather than a throw.
|
|
283
|
+
*
|
|
263
284
|
* @param value - The value to test
|
|
264
285
|
* @returns True if `value` is a complete {@link MigrationInput}; false otherwise
|
|
265
286
|
*/
|
|
266
287
|
function isMigrationInput(value) {
|
|
267
|
-
|
|
288
|
+
return holds(() => {
|
|
268
289
|
const input = cloneJSONRecord(value);
|
|
269
290
|
const keys = Object.keys(input);
|
|
270
291
|
return (keys.length === 1 || keys.length === 2) && keys.includes("plan") && (keys.length === 1 || keys.includes("metadata")) && isMigration(input.plan) && (input.metadata === void 0 || isDriverMetadata(input.metadata));
|
|
271
|
-
}
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
292
|
+
});
|
|
274
293
|
}
|
|
275
294
|
//#endregion
|
|
276
295
|
//#region src/core/cloners.ts
|
|
277
296
|
/**
|
|
278
297
|
* Clones unknown driver metadata into a distinct deeply frozen snapshot.
|
|
279
298
|
*
|
|
299
|
+
* @remarks
|
|
300
|
+
* The clone is validated as {@link DriverMetadata} before it is returned, so a
|
|
301
|
+
* malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
|
|
302
|
+
* `context.path === 'metadata'` rather than surfacing a raw Contract or caller error.
|
|
303
|
+
*
|
|
280
304
|
* @param value - Unknown metadata
|
|
281
305
|
* @returns Owned driver metadata
|
|
282
306
|
*/
|
|
@@ -286,7 +310,7 @@ function cloneDriverMetadata(value) {
|
|
|
286
310
|
if (isDriverMetadata(metadata)) return metadata;
|
|
287
311
|
throw new DatabaseError("VALIDATION", "Driver metadata is invalid", { path: "metadata" });
|
|
288
312
|
} catch (error) {
|
|
289
|
-
if (error
|
|
313
|
+
if (isDatabaseError(error)) throw error;
|
|
290
314
|
throw new DatabaseError("VALIDATION", "Driver metadata is invalid", {
|
|
291
315
|
path: "metadata",
|
|
292
316
|
cause: error
|
|
@@ -296,6 +320,11 @@ function cloneDriverMetadata(value) {
|
|
|
296
320
|
/**
|
|
297
321
|
* Clones unknown driver schema into a distinct deeply frozen snapshot.
|
|
298
322
|
*
|
|
323
|
+
* @remarks
|
|
324
|
+
* The clone is validated as a table-schema collection before it is returned, so a
|
|
325
|
+
* malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
|
|
326
|
+
* `context.path === 'schema'` rather than surfacing a raw Contract or caller error.
|
|
327
|
+
*
|
|
299
328
|
* @param value - Unknown table schema collection
|
|
300
329
|
* @returns Owned driver schema
|
|
301
330
|
*/
|
|
@@ -305,7 +334,7 @@ function cloneDriverSchema(value) {
|
|
|
305
334
|
if (isDriverSchema(schema)) return schema;
|
|
306
335
|
throw new DatabaseError("VALIDATION", "Driver schema is invalid", { path: "schema" });
|
|
307
336
|
} catch (error) {
|
|
308
|
-
if (error
|
|
337
|
+
if (isDatabaseError(error)) throw error;
|
|
309
338
|
throw new DatabaseError("VALIDATION", "Driver schema is invalid", {
|
|
310
339
|
path: "schema",
|
|
311
340
|
cause: error
|
|
@@ -315,6 +344,11 @@ function cloneDriverSchema(value) {
|
|
|
315
344
|
/**
|
|
316
345
|
* Clones unknown migration input into a distinct deeply frozen snapshot.
|
|
317
346
|
*
|
|
347
|
+
* @remarks
|
|
348
|
+
* The clone is validated as a {@link MigrationInput} before it is returned, so a
|
|
349
|
+
* malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
|
|
350
|
+
* `context.path === 'migration'` rather than surfacing a raw Contract or caller error.
|
|
351
|
+
*
|
|
318
352
|
* @param value - Unknown migration input
|
|
319
353
|
* @returns Owned migration input
|
|
320
354
|
*/
|
|
@@ -324,7 +358,7 @@ function cloneMigrationInput(value) {
|
|
|
324
358
|
if (isMigrationInput(input)) return input;
|
|
325
359
|
throw new DatabaseError("VALIDATION", "Migration input is invalid", { path: "migration" });
|
|
326
360
|
} catch (error) {
|
|
327
|
-
if (error
|
|
361
|
+
if (isDatabaseError(error)) throw error;
|
|
328
362
|
throw new DatabaseError("VALIDATION", "Migration input is invalid", {
|
|
329
363
|
path: "migration",
|
|
330
364
|
cause: error
|
|
@@ -347,14 +381,14 @@ function cloneMigrationInput(value) {
|
|
|
347
381
|
*/
|
|
348
382
|
function validatePage(input) {
|
|
349
383
|
const limit = input?.limit;
|
|
350
|
-
if (limit !== void 0 && (!
|
|
384
|
+
if (limit !== void 0 && (!isInteger(limit) || limit < 0)) throw new DatabaseError("VALIDATION", "Query limit must be a nonnegative integer", {
|
|
351
385
|
field: "limit",
|
|
352
|
-
value:
|
|
386
|
+
value: isFiniteNumber(limit) ? limit : String(limit)
|
|
353
387
|
});
|
|
354
388
|
const offset = input?.offset;
|
|
355
|
-
if (offset !== void 0 && (!
|
|
389
|
+
if (offset !== void 0 && (!isInteger(offset) || offset < 0)) throw new DatabaseError("VALIDATION", "Query offset must be a nonnegative integer", {
|
|
356
390
|
field: "offset",
|
|
357
|
-
value:
|
|
391
|
+
value: isFiniteNumber(offset) ? offset : String(offset)
|
|
358
392
|
});
|
|
359
393
|
}
|
|
360
394
|
/**
|
|
@@ -372,14 +406,14 @@ function validatePage(input) {
|
|
|
372
406
|
* @returns `-1`, `0`, or `1`
|
|
373
407
|
*/
|
|
374
408
|
function compareValues(left, right) {
|
|
375
|
-
const [leftRank = 5, rightRank = 5] = [left, right].map((value) => value === void 0 ? 0 : value === null ? 1 :
|
|
409
|
+
const [leftRank = 5, rightRank = 5] = [left, right].map((value) => value === void 0 ? 0 : value === null ? 1 : isBoolean(value) ? 2 : isNumber(value) ? 3 : isString(value) ? 4 : 5);
|
|
376
410
|
if (leftRank !== rightRank) return leftRank < rightRank ? -1 : 1;
|
|
377
|
-
if (
|
|
411
|
+
if (isNumber(left) && isNumber(right)) {
|
|
378
412
|
if (Number.isNaN(left) || Number.isNaN(right)) return Number.isNaN(left) ? Number.isNaN(right) ? 0 : 1 : -1;
|
|
379
413
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
380
414
|
}
|
|
381
|
-
if (
|
|
382
|
-
if (
|
|
415
|
+
if (isString(left) && isString(right)) return left < right ? -1 : left > right ? 1 : 0;
|
|
416
|
+
if (isBoolean(left) && isBoolean(right)) return left === right ? 0 : left ? 1 : -1;
|
|
383
417
|
return 0;
|
|
384
418
|
}
|
|
385
419
|
/**
|
|
@@ -390,9 +424,9 @@ function compareValues(left, right) {
|
|
|
390
424
|
* @remarks
|
|
391
425
|
* Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
|
|
392
426
|
* Arrays compare by index (same length, every element `equalsValue`). Plain
|
|
393
|
-
* records (through `isRecord`) compare by their
|
|
394
|
-
*
|
|
395
|
-
* with a `equalsValue` value — so a key present with value `undefined` is
|
|
427
|
+
* records (through `isRecord`) compare by their own enumerable keys: same key
|
|
428
|
+
* count and, for every key in `left`, `right` has that key (`Object.hasOwn`)
|
|
429
|
+
* with a `equalsValue` value — so a key present with value `undefined` is not
|
|
396
430
|
* equal to that key being absent (both differ in `Object.keys` membership).
|
|
397
431
|
* Anything else (functions, class instances, mismatched shapes) falls through
|
|
398
432
|
* to `false`. Container pairs are tracked iteratively, so self-referential and
|
|
@@ -418,13 +452,13 @@ function equalsValue(left, right) {
|
|
|
418
452
|
const pair = pending.pop();
|
|
419
453
|
if (pair === void 0) continue;
|
|
420
454
|
const [currentLeft, currentRight] = pair;
|
|
421
|
-
if (
|
|
455
|
+
if (isNumber(currentLeft) && isNumber(currentRight)) {
|
|
422
456
|
if (Number.isNaN(currentLeft) && Number.isNaN(currentRight) || currentLeft === currentRight) continue;
|
|
423
457
|
return false;
|
|
424
458
|
}
|
|
425
459
|
if (currentLeft === currentRight) continue;
|
|
426
|
-
const leftArray =
|
|
427
|
-
const rightArray =
|
|
460
|
+
const leftArray = isArray(currentLeft);
|
|
461
|
+
const rightArray = isArray(currentRight);
|
|
428
462
|
const leftRecord = isRecord(currentLeft);
|
|
429
463
|
const rightRecord = isRecord(currentRight);
|
|
430
464
|
if (leftArray !== rightArray || leftRecord !== rightRecord) return false;
|
|
@@ -458,16 +492,16 @@ function equalsValue(left, right) {
|
|
|
458
492
|
}
|
|
459
493
|
}
|
|
460
494
|
/**
|
|
461
|
-
* Matches a value against a wildcard pattern in
|
|
495
|
+
* Matches a value against a wildcard pattern in linear time — the shared, ReDoS-safe
|
|
462
496
|
* engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
|
|
463
497
|
*
|
|
464
498
|
* @remarks
|
|
465
|
-
* A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is
|
|
499
|
+
* A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is catastrophic on a hostile pattern:
|
|
466
500
|
* `.*` segments separated by literals, matched against a long non-matching input, blow
|
|
467
501
|
* up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it,
|
|
468
502
|
* while a `LIKE` / `GLOB` pattern is a caller-supplied operand this package cannot
|
|
469
|
-
* trust. So this builds
|
|
470
|
-
* the `any` wildcard records its position and, on a later mismatch, backtracks
|
|
503
|
+
* trust. So this builds no regex. It runs the classic greedy two-pointer wildcard match:
|
|
504
|
+
* the `any` wildcard records its position and, on a later mismatch, backtracks only to
|
|
471
505
|
* that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
|
|
472
506
|
* never the exponential / polynomial backtracking a regex would do. The pattern length
|
|
473
507
|
* is capped at {@link MAX_PATTERN_LENGTH} (a `VALIDATION` {@link DatabaseError} over it),
|
|
@@ -475,16 +509,16 @@ function equalsValue(left, right) {
|
|
|
475
509
|
* pattern.
|
|
476
510
|
*
|
|
477
511
|
* The `any` wildcard matches any run (including empty); `single` matches exactly one
|
|
478
|
-
* char; every other pattern char matches itself
|
|
512
|
+
* char; every other pattern char matches itself literally (a pattern `.` / `(` / `\` is
|
|
479
513
|
* a literal — the regex-metacharacter hazard is gone with the regex). `any` is tested
|
|
480
|
-
*
|
|
481
|
-
* shadows the wildcard. Case folding is applied to
|
|
514
|
+
* before a literal match, so a value that literally contains the wildcard char never
|
|
515
|
+
* shadows the wildcard. Case folding is applied to both sides when `fold` is set.
|
|
482
516
|
*
|
|
483
517
|
* @param value - The value to test
|
|
484
518
|
* @param pattern - The wildcard pattern
|
|
485
519
|
* @param any - The any-run wildcard char (`%` for `LIKE`, `*` for `GLOB`)
|
|
486
520
|
* @param single - The single-char wildcard char (`_` for `LIKE`, `?` for `GLOB`)
|
|
487
|
-
* @param fold - Whether to match case-
|
|
521
|
+
* @param fold - Whether to match case-insensitively (`LIKE` folds; `GLOB` does not)
|
|
488
522
|
* @returns True if `value` matches `pattern`; false otherwise
|
|
489
523
|
* @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
|
|
490
524
|
*/
|
|
@@ -546,7 +580,7 @@ function matchesLikePattern(value, pattern) {
|
|
|
546
580
|
* @remarks
|
|
547
581
|
* `*` matches any run of characters (including none) and `?` matches exactly one
|
|
548
582
|
* character; every other pattern character matches itself literally, so a
|
|
549
|
-
* character class such as `[a-z]` is
|
|
583
|
+
* character class such as `[a-z]` is not interpreted. Runs on
|
|
550
584
|
* {@link matchesWildcardPattern}, so the match is linear in the value length and
|
|
551
585
|
* the pattern is capped at {@link MAX_PATTERN_LENGTH}.
|
|
552
586
|
*
|
|
@@ -572,7 +606,7 @@ function matchesGlobPattern(value, pattern) {
|
|
|
572
606
|
* string is one column; an array descends a nested value) — and applies the
|
|
573
607
|
* operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
|
|
574
608
|
* {@link compareValues}, the total order; the equality family (`equals` / `not`
|
|
575
|
-
* / `any` / `none`) uses {@link equalsValue} —
|
|
609
|
+
* / `any` / `none`) uses {@link equalsValue} — structural equality, not the total
|
|
576
610
|
* order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
|
|
577
611
|
* operand only matches a structurally-equal value, never every row holding any
|
|
578
612
|
* object. This is a semantics change from ranking: `equalsValue` is SameValueZero
|
|
@@ -730,7 +764,9 @@ function computeAggregate(rows, operation, column) {
|
|
|
730
764
|
const total = numbers.reduce((sum, value) => sum + value, 0);
|
|
731
765
|
return operation === "average" ? total / numbers.length : total;
|
|
732
766
|
}
|
|
733
|
-
|
|
767
|
+
let result = operation === "minimum" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
|
768
|
+
for (const value of numbers) result = operation === "minimum" ? Math.min(result, value) : Math.max(result, value);
|
|
769
|
+
return result;
|
|
734
770
|
}
|
|
735
771
|
/**
|
|
736
772
|
* Reads a row's primary key from a column, when it is a usable {@link Key}.
|
|
@@ -787,8 +823,8 @@ function shapeToColumnStorage(shape) {
|
|
|
787
823
|
case "number": return shape.integer === true ? "integer" : "real";
|
|
788
824
|
case "boolean": return "boolean";
|
|
789
825
|
case "literal":
|
|
790
|
-
if (shape.values.every((value) =>
|
|
791
|
-
if (shape.values.every((value) =>
|
|
826
|
+
if (shape.values.every((value) => isBoolean(value))) return "boolean";
|
|
827
|
+
if (shape.values.every((value) => isNumber(value))) return shape.values.every((value) => isInteger(value)) ? "integer" : "real";
|
|
792
828
|
return "text";
|
|
793
829
|
case "optional":
|
|
794
830
|
case "nullable": return shapeToColumnStorage(shape.inner);
|
|
@@ -908,16 +944,16 @@ function checkAbort(signal) {
|
|
|
908
944
|
* plan labels only; versioning drivers persist and reconcile them through
|
|
909
945
|
* {@link DriverMetadata}.
|
|
910
946
|
*
|
|
911
|
-
* A column present in
|
|
947
|
+
* A column present in both schemas under the same name but with a different
|
|
912
948
|
* `storage`, `optional`, or `nullable` value throws a `MIGRATION`
|
|
913
949
|
* {@link DatabaseError} naming the table, the column, and the from→to
|
|
914
|
-
* difference — a name-only diff would otherwise silently produce
|
|
950
|
+
* difference — a name-only diff would otherwise silently produce no step for
|
|
915
951
|
* the drift, and versioned reconciliation would stamp over it. There is no
|
|
916
952
|
* automatic in-place type-change step: the manual path is to add a new column,
|
|
917
953
|
* copy/convert the data at the application layer, then remove the old column —
|
|
918
954
|
* two separate plans, never a single implicit "alter" step.
|
|
919
955
|
*
|
|
920
|
-
* @param deployed - The table schemas
|
|
956
|
+
* @param deployed - The already-applied table schemas
|
|
921
957
|
* @param declared - The table schemas the caller wants applied
|
|
922
958
|
* @param from - The plan's source version label (defaults to `0`)
|
|
923
959
|
* @param to - The plan's target version label (defaults to `1`)
|
|
@@ -937,7 +973,7 @@ function checkAbort(signal) {
|
|
|
937
973
|
* ```
|
|
938
974
|
*/
|
|
939
975
|
function planMigration(deployed, declared, from = 0, to = 1) {
|
|
940
|
-
if (!
|
|
976
|
+
if (!isFiniteNumber(from) || !isFiniteNumber(to)) throw new DatabaseError("MIGRATION", "Migration versions must be finite", {
|
|
941
977
|
from,
|
|
942
978
|
to
|
|
943
979
|
});
|
|
@@ -1024,6 +1060,8 @@ function planMigration(deployed, declared, from = 0, to = 1) {
|
|
|
1024
1060
|
}
|
|
1025
1061
|
/**
|
|
1026
1062
|
* Projects migration steps sequentially over a canonical validated owned schema.
|
|
1063
|
+
*
|
|
1064
|
+
* @remarks
|
|
1027
1065
|
* Adding a required non-null column to an existing table rejects with
|
|
1028
1066
|
* `MIGRATION`; optional-only and nullable-only additions remain portable.
|
|
1029
1067
|
*
|
|
@@ -1187,14 +1225,14 @@ function migrateRows(rows, steps) {
|
|
|
1187
1225
|
* default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
|
|
1188
1226
|
* fresh for each phase so failures stay isolated, verifies: `open`/`close`;
|
|
1189
1227
|
* `read` of a missing key returns `undefined`; `write`/`read` round-trip
|
|
1190
|
-
* with
|
|
1191
|
-
* including a
|
|
1228
|
+
* with deep copy-in/copy-out isolation (mutating the caller's row —
|
|
1229
|
+
* including a nested field — after `write`, or a row `read` returns, never
|
|
1192
1230
|
* perturbs stored state) and upsert-overwrite; simultaneous same-key
|
|
1193
1231
|
* `insert` calls produce exactly one commit and one `CONFLICT`; pre-aborted
|
|
1194
1232
|
* `write`, `insert`, and `delete` calls leave storage unchanged; `delete`
|
|
1195
1233
|
* returns `true` then `false`; `keys`/`scan` yield in ascending key order;
|
|
1196
1234
|
* `clear` empties only its target table; `snapshot`'s rollback thunk
|
|
1197
|
-
* restores pre-snapshot state, including a
|
|
1235
|
+
* restores pre-snapshot state, including a nested field mutated in place on
|
|
1198
1236
|
* a read-back row between capture and restore; a scoped
|
|
1199
1237
|
* `snapshot(['users'])` rolls back only the named table, leaving a
|
|
1200
1238
|
* concurrent mutation to another table intact; a non-`id` primary key
|
|
@@ -1208,12 +1246,12 @@ function migrateRows(rows, steps) {
|
|
|
1208
1246
|
* store's `metadata()` is `undefined`, and after
|
|
1209
1247
|
* `stamp({ version, schema })`, `metadata()` returns the exact stamped value.
|
|
1210
1248
|
*
|
|
1211
|
-
* Each phase runs within a `try`/`catch`: an
|
|
1212
|
-
* finding built from the assertion, while an
|
|
1249
|
+
* Each phase runs within a `try`/`catch`: an expected mismatch yields a
|
|
1250
|
+
* finding built from the assertion, while an unexpected throw (a driver
|
|
1213
1251
|
* crash mid-phase) is caught and yielded as a finding too, naming the phase
|
|
1214
1252
|
* as `check` and carrying the caught error in `context.error` — a broken
|
|
1215
1253
|
* driver can never escape the battery as an unhandled rejection. Within a
|
|
1216
|
-
* phase, the
|
|
1254
|
+
* phase, the first violated assertion yields and the phase stops (matching
|
|
1217
1255
|
* the historical fail-fast shape at phase granularity); the generator then
|
|
1218
1256
|
* moves on to the next phase regardless. Because this is a **generator**,
|
|
1219
1257
|
* consuming only the first yielded value reproduces true fail-fast (later
|
|
@@ -1239,7 +1277,7 @@ async function* scanDriver(factory) {
|
|
|
1239
1277
|
} catch (error) {
|
|
1240
1278
|
yield {
|
|
1241
1279
|
check: "open-close",
|
|
1242
|
-
message: error
|
|
1280
|
+
message: isError(error) ? error.message : String(error),
|
|
1243
1281
|
context: { error }
|
|
1244
1282
|
};
|
|
1245
1283
|
}
|
|
@@ -1260,7 +1298,7 @@ async function* scanDriver(factory) {
|
|
|
1260
1298
|
} catch (error) {
|
|
1261
1299
|
yield {
|
|
1262
1300
|
check: "read-missing",
|
|
1263
|
-
message: error
|
|
1301
|
+
message: isError(error) ? error.message : String(error),
|
|
1264
1302
|
context: { error }
|
|
1265
1303
|
};
|
|
1266
1304
|
}
|
|
@@ -1336,7 +1374,7 @@ async function* scanDriver(factory) {
|
|
|
1336
1374
|
} catch (error) {
|
|
1337
1375
|
yield {
|
|
1338
1376
|
check: "write-read",
|
|
1339
|
-
message: error
|
|
1377
|
+
message: isError(error) ? error.message : String(error),
|
|
1340
1378
|
context: { error }
|
|
1341
1379
|
};
|
|
1342
1380
|
}
|
|
@@ -1377,7 +1415,7 @@ async function* scanDriver(factory) {
|
|
|
1377
1415
|
} catch (error) {
|
|
1378
1416
|
yield {
|
|
1379
1417
|
check: "insert-atomic",
|
|
1380
|
-
message: error
|
|
1418
|
+
message: isError(error) ? error.message : String(error),
|
|
1381
1419
|
context: { error }
|
|
1382
1420
|
};
|
|
1383
1421
|
}
|
|
@@ -1417,7 +1455,7 @@ async function* scanDriver(factory) {
|
|
|
1417
1455
|
} catch (error) {
|
|
1418
1456
|
yield {
|
|
1419
1457
|
check: "delete",
|
|
1420
|
-
message: error
|
|
1458
|
+
message: isError(error) ? error.message : String(error),
|
|
1421
1459
|
context: { error }
|
|
1422
1460
|
};
|
|
1423
1461
|
}
|
|
@@ -1480,7 +1518,7 @@ async function* scanDriver(factory) {
|
|
|
1480
1518
|
} catch (error) {
|
|
1481
1519
|
yield {
|
|
1482
1520
|
check: "mutation-abort",
|
|
1483
|
-
message: error
|
|
1521
|
+
message: isError(error) ? error.message : String(error),
|
|
1484
1522
|
context: { error }
|
|
1485
1523
|
};
|
|
1486
1524
|
}
|
|
@@ -1539,7 +1577,7 @@ async function* scanDriver(factory) {
|
|
|
1539
1577
|
} catch (error) {
|
|
1540
1578
|
yield {
|
|
1541
1579
|
check: "order",
|
|
1542
|
-
message: error
|
|
1580
|
+
message: isError(error) ? error.message : String(error),
|
|
1543
1581
|
context: { error }
|
|
1544
1582
|
};
|
|
1545
1583
|
}
|
|
@@ -1583,7 +1621,7 @@ async function* scanDriver(factory) {
|
|
|
1583
1621
|
} catch (error) {
|
|
1584
1622
|
yield {
|
|
1585
1623
|
check: "clear",
|
|
1586
|
-
message: error
|
|
1624
|
+
message: isError(error) ? error.message : String(error),
|
|
1587
1625
|
context: { error }
|
|
1588
1626
|
};
|
|
1589
1627
|
}
|
|
@@ -1632,7 +1670,7 @@ async function* scanDriver(factory) {
|
|
|
1632
1670
|
} catch (error) {
|
|
1633
1671
|
yield {
|
|
1634
1672
|
check: "snapshot",
|
|
1635
|
-
message: error
|
|
1673
|
+
message: isError(error) ? error.message : String(error),
|
|
1636
1674
|
context: { error }
|
|
1637
1675
|
};
|
|
1638
1676
|
}
|
|
@@ -1670,7 +1708,7 @@ async function* scanDriver(factory) {
|
|
|
1670
1708
|
} catch (error) {
|
|
1671
1709
|
yield {
|
|
1672
1710
|
check: "snapshot-nested",
|
|
1673
|
-
message: error
|
|
1711
|
+
message: isError(error) ? error.message : String(error),
|
|
1674
1712
|
context: { error }
|
|
1675
1713
|
};
|
|
1676
1714
|
}
|
|
@@ -1696,7 +1734,7 @@ async function* scanDriver(factory) {
|
|
|
1696
1734
|
} catch (error) {
|
|
1697
1735
|
yield {
|
|
1698
1736
|
check: "non-id-primary",
|
|
1699
|
-
message: error
|
|
1737
|
+
message: isError(error) ? error.message : String(error),
|
|
1700
1738
|
context: { error }
|
|
1701
1739
|
};
|
|
1702
1740
|
}
|
|
@@ -1727,7 +1765,7 @@ async function* scanDriver(factory) {
|
|
|
1727
1765
|
} catch (error) {
|
|
1728
1766
|
yield {
|
|
1729
1767
|
check: "nested-roundtrip",
|
|
1730
|
-
message: error
|
|
1768
|
+
message: isError(error) ? error.message : String(error),
|
|
1731
1769
|
context: { error }
|
|
1732
1770
|
};
|
|
1733
1771
|
}
|
|
@@ -1792,7 +1830,7 @@ async function* scanDriver(factory) {
|
|
|
1792
1830
|
} catch (error) {
|
|
1793
1831
|
yield {
|
|
1794
1832
|
check: "migrate",
|
|
1795
|
-
message: error
|
|
1833
|
+
message: isError(error) ? error.message : String(error),
|
|
1796
1834
|
context: { error }
|
|
1797
1835
|
};
|
|
1798
1836
|
}
|
|
@@ -1857,7 +1895,7 @@ async function* scanDriver(factory) {
|
|
|
1857
1895
|
} catch (error) {
|
|
1858
1896
|
yield {
|
|
1859
1897
|
check: "stream",
|
|
1860
|
-
message: error
|
|
1898
|
+
message: isError(error) ? error.message : String(error),
|
|
1861
1899
|
context: { error }
|
|
1862
1900
|
};
|
|
1863
1901
|
}
|
|
@@ -1918,7 +1956,7 @@ async function* scanDriver(factory) {
|
|
|
1918
1956
|
} catch (error) {
|
|
1919
1957
|
yield {
|
|
1920
1958
|
check: "transaction",
|
|
1921
|
-
message: error
|
|
1959
|
+
message: isError(error) ? error.message : String(error),
|
|
1922
1960
|
context: { error }
|
|
1923
1961
|
};
|
|
1924
1962
|
}
|
|
@@ -1957,7 +1995,7 @@ async function* scanDriver(factory) {
|
|
|
1957
1995
|
} catch (error) {
|
|
1958
1996
|
yield {
|
|
1959
1997
|
check: "metadata-stamp",
|
|
1960
|
-
message: error
|
|
1998
|
+
message: isError(error) ? error.message : String(error),
|
|
1961
1999
|
context: { error }
|
|
1962
2000
|
};
|
|
1963
2001
|
}
|
|
@@ -2012,7 +2050,7 @@ async function* scanDriver(factory) {
|
|
|
2012
2050
|
} catch (error) {
|
|
2013
2051
|
yield {
|
|
2014
2052
|
check: "snapshot-scoped",
|
|
2015
|
-
message: error
|
|
2053
|
+
message: isError(error) ? error.message : String(error),
|
|
2016
2054
|
context: { error }
|
|
2017
2055
|
};
|
|
2018
2056
|
}
|
|
@@ -2024,11 +2062,13 @@ async function* scanDriver(factory) {
|
|
|
2024
2062
|
*
|
|
2025
2063
|
* @remarks
|
|
2026
2064
|
* Consumes only the first value {@link scanDriver} yields: because that
|
|
2027
|
-
* generator is lazy, every
|
|
2065
|
+
* generator is lazy, every later phase never runs — true fail-fast, not
|
|
2028
2066
|
* merely "report only the first". The
|
|
2029
2067
|
* thrown error is byte-compatible with the historical shape: a
|
|
2030
2068
|
* `CONFORMANCE` {@link DatabaseError} whose `message` is the finding's
|
|
2031
|
-
* `message` and whose `context` is `{ check, ...finding.context }`.
|
|
2069
|
+
* `message` and whose `context` is `{ check, ...finding.context }`. The battery
|
|
2070
|
+
* takes a driver factory and reports through a throw, so it binds no test
|
|
2071
|
+
* framework and runs from any runner.
|
|
2032
2072
|
*
|
|
2033
2073
|
* @param factory - Mints a fresh, unopened driver instance (called once per phase)
|
|
2034
2074
|
* @returns Nothing — resolves once every phase has passed
|
|
@@ -2048,7 +2088,7 @@ async function conformDriver(factory) {
|
|
|
2048
2088
|
});
|
|
2049
2089
|
}
|
|
2050
2090
|
/**
|
|
2051
|
-
* Runs the
|
|
2091
|
+
* Runs the full driver-conformance battery and collects every violation — the
|
|
2052
2092
|
* audit entry point for a driver author who wants a complete report rather
|
|
2053
2093
|
* than a single fail-fast throw.
|
|
2054
2094
|
*
|
|
@@ -2718,10 +2758,10 @@ var Query = class {
|
|
|
2718
2758
|
* @remarks
|
|
2719
2759
|
* - **Observable.** The owned {@link emitter} ({@link TableEventMap}) carries the
|
|
2720
2760
|
* per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
|
|
2721
|
-
* fire-and-forget observers (cache invalidation, sync, an audit log),
|
|
2722
|
-
* database-level lifecycle. Events carry the affected
|
|
2761
|
+
* fire-and-forget observers (cache invalidation, sync, an audit log), alongside the
|
|
2762
|
+
* database-level lifecycle. Events carry the affected key only (no value payload, to
|
|
2723
2763
|
* keep fan-out lean); reads / queries / counts are not emitted. Every event is emitted
|
|
2724
|
-
* directly, strictly
|
|
2764
|
+
* directly, strictly after the driver write / delete / clear completes; the emitter
|
|
2725
2765
|
* isolates a listener throw and routes it to its `error` handler (the `error` option),
|
|
2726
2766
|
* so a buggy observer can never corrupt a write or perturb a transaction.
|
|
2727
2767
|
*/
|
|
@@ -2834,7 +2874,7 @@ var Table = class {
|
|
|
2834
2874
|
* conditions.
|
|
2835
2875
|
*
|
|
2836
2876
|
* @remarks
|
|
2837
|
-
* Unlike {@link count}, `aggregate` operates on
|
|
2877
|
+
* Unlike {@link count}, `aggregate` operates on stored rows without the
|
|
2838
2878
|
* contract guard {@link records} / {@link scan} apply — a non-conforming
|
|
2839
2879
|
* stored row still contributes to the computed aggregate when it matches
|
|
2840
2880
|
* the conditions, even though it would never appear in `records()`'s
|
|
@@ -3162,6 +3202,10 @@ var DatabaseTransaction = class {
|
|
|
3162
3202
|
* generator. Imported views register their physical schemas with the same
|
|
3163
3203
|
* internal context before opening begins, so every view observes one driver,
|
|
3164
3204
|
* merged schema, emitter, status, transaction boundary, and terminal close.
|
|
3205
|
+
*
|
|
3206
|
+
* The view owns the driver and its declared `tables`, connects that driver lazily on
|
|
3207
|
+
* first use, `import`s further tables and `export`s their portable definitions, and
|
|
3208
|
+
* runs `transaction` scopes over the shared context.
|
|
3165
3209
|
*/
|
|
3166
3210
|
var Database = class Database {
|
|
3167
3211
|
#context;
|
|
@@ -3284,7 +3328,7 @@ var Database = class Database {
|
|
|
3284
3328
|
* @remarks
|
|
3285
3329
|
* The in-between made concrete: it runs identically in a browser or on a server,
|
|
3286
3330
|
* so it is the storage behind tests, ephemeral caches, and any code that wants
|
|
3287
|
-
* the database API without a persistent backend. Rows are
|
|
3331
|
+
* the database API without a persistent backend. Rows are deep-copied (through
|
|
3288
3332
|
* `structuredClone`) in and out — at `write`, `read`, `scan`, `stream`, and both
|
|
3289
3333
|
* snapshot capture and restore — so a caller mutating a nested field of an input
|
|
3290
3334
|
* row, a returned row, or a row mutated in place between snapshot and rollback
|
|
@@ -3359,7 +3403,7 @@ var MemoryDriver = class {
|
|
|
3359
3403
|
* toward `offset` / `limit`. Both are applied lazily as matches are found —
|
|
3360
3404
|
* `offset` matches are skipped without being yielded, and iteration stops the
|
|
3361
3405
|
* instant `limit` yields have been produced, so a large table is never fully
|
|
3362
|
-
* walked for a small page. `input.order` is
|
|
3406
|
+
* walked for a small page. `input.order` is ignored (the same contract as
|
|
3363
3407
|
* `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
|
|
3364
3408
|
* order, sorted output is `records()`'s job. Rows yield copy-out, and an
|
|
3365
3409
|
* unknown table mirrors `scan`'s empty-yield behavior.
|
|
@@ -3588,20 +3632,29 @@ var MemoryDriver = class {
|
|
|
3588
3632
|
* `name`, `generator`, `version`, and emitter hooks
|
|
3589
3633
|
* @returns A typed {@link DatabaseInterface}
|
|
3590
3634
|
*
|
|
3591
|
-
* @example
|
|
3635
|
+
* @example Create a database
|
|
3592
3636
|
* ```ts
|
|
3593
3637
|
* import { createDatabase, createMemoryDriver } from '@orkestrel/database'
|
|
3594
3638
|
* import { integerShape, stringShape } from '@orkestrel/contract'
|
|
3595
3639
|
*
|
|
3596
3640
|
* const db = createDatabase({
|
|
3597
|
-
* driver: createMemoryDriver(),
|
|
3641
|
+
* driver: createMemoryDriver(), // any DriverInterface — a persistent backend swaps in, same API
|
|
3598
3642
|
* tables: {
|
|
3599
|
-
* users: { id: stringShape(), age: integerShape() },
|
|
3643
|
+
* users: { id: stringShape(), name: stringShape(), age: integerShape() },
|
|
3600
3644
|
* posts: { slug: stringShape(), title: stringShape() },
|
|
3601
3645
|
* },
|
|
3602
|
-
* primary: { posts: 'slug' },
|
|
3646
|
+
* primary: { posts: 'slug' }, // non-`id` primary-key columns, per table
|
|
3603
3647
|
* })
|
|
3604
|
-
*
|
|
3648
|
+
*
|
|
3649
|
+
* const users = db.table('users') // hold the handle; TableInterface<{ id; name; age }>
|
|
3650
|
+
*
|
|
3651
|
+
* await users.set({ id: 'u1', name: 'Ada', age: 36 }) // coerced + validated through the contract
|
|
3652
|
+
* await users.get('u1') // typed { id; name; age } | undefined — narrowed, never `as`
|
|
3653
|
+
* await users
|
|
3654
|
+
* .query()
|
|
3655
|
+
* .condition({ column: 'age', operator: 'from', values: [18], connector: 'and' })
|
|
3656
|
+
* .order({ column: 'age', direction: 'descending' })
|
|
3657
|
+
* .collect() // typed rows
|
|
3605
3658
|
* ```
|
|
3606
3659
|
*/
|
|
3607
3660
|
function createDatabase(options) {
|