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