@jarenjs/db 0.49.2 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +27 -15
- package/README.md +141 -41
- package/docs/JOBS-FORMAT.md +24 -8
- package/docs/LIVE-FORMAT.md +38 -9
- package/docs/MIGRATION-FORMAT.md +118 -36
- package/docs/MODEL-FORMAT.md +232 -30
- package/package.json +4 -5
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/src/capture.js +66 -28
- package/src/cli.js +225 -44
- package/src/ddl.js +23 -3
- package/src/dialects/sqlite.js +2 -1
- package/src/driver.js +63 -16
- package/src/drivers/wasm.js +1 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +10 -3
- package/src/entity.js +92 -47
- package/src/errors.js +25 -0
- package/src/index.js +2 -2
- package/src/jobs.js +40 -5
- package/src/live-time.js +12 -3
- package/src/live.js +11 -1
- package/src/migrate.js +397 -191
- package/src/model.js +173 -8
- package/src/plan.js +135 -38
- package/src/query.js +138 -13
- package/src/store.js +221 -66
- package/src/tracker.js +173 -48
- package/types/index.d.ts +152 -10
- package/types/node.d.ts +3 -1
- package/types/typed.d.ts +58 -2
- package/types/wasm.d.ts +7 -0
- package/dist/types/algebra.d.ts +0 -230
- package/dist/types/app.d.ts +0 -49
- package/dist/types/capture.d.ts +0 -85
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/dag-job.d.ts +0 -40
- package/dist/types/ddl.d.ts +0 -229
- package/dist/types/derive.d.ts +0 -250
- package/dist/types/dialect.d.ts +0 -154
- package/dist/types/dialects/sqlite.d.ts +0 -9
- package/dist/types/driver.d.ts +0 -110
- package/dist/types/drivers/bun.d.ts +0 -47
- package/dist/types/drivers/node.d.ts +0 -37
- package/dist/types/drivers/wasm.d.ts +0 -65
- package/dist/types/emit-model.d.ts +0 -44
- package/dist/types/emit.d.ts +0 -75
- package/dist/types/entity.d.ts +0 -23
- package/dist/types/errors.d.ts +0 -170
- package/dist/types/graph.d.ts +0 -28
- package/dist/types/index.d.ts +0 -37
- package/dist/types/jobs.d.ts +0 -140
- package/dist/types/knn.d.ts +0 -69
- package/dist/types/live-time.d.ts +0 -141
- package/dist/types/live.d.ts +0 -64
- package/dist/types/migrate.d.ts +0 -170
- package/dist/types/model.d.ts +0 -36
- package/dist/types/patch-sql.d.ts +0 -37
- package/dist/types/plan.d.ts +0 -142
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -112
- package/dist/types/residual.d.ts +0 -64
- package/dist/types/series.d.ts +0 -227
- package/dist/types/store.d.ts +0 -60
- package/dist/types/tracker.d.ts +0 -43
- package/dist/types/typed.d.ts +0 -15
- package/dist/types/types.d.ts +0 -26
- package/dist/types/udf.d.ts +0 -75
- package/dist/types/window.d.ts +0 -52
package/src/store.js
CHANGED
|
@@ -23,20 +23,20 @@
|
|
|
23
23
|
import { applyJSONPatch } from '@jarenjs/json/patch';
|
|
24
24
|
import { parseJSONPointer } from '@jarenjs/json/pointer';
|
|
25
25
|
|
|
26
|
-
import { DbCompileError, DbRuntimeError } from './errors.js';
|
|
27
|
-
import { chain, toPromise, isThenable } from './driver.js';
|
|
26
|
+
import { DbCompileError, DbRuntimeError, isDuplicateKeyError } from './errors.js';
|
|
27
|
+
import { chain, toPromise, isThenable, attempt } from './driver.js';
|
|
28
28
|
import { planCollection, planEntity, planJoinTable, verifyShape } from './ddl.js';
|
|
29
29
|
import { translatePatch } from './patch-sql.js';
|
|
30
30
|
import { createQueryEngine, createQueryState, createEntityQueryEngine, createLoadEngine } from './query.js';
|
|
31
31
|
import { normalizeProfile } from './profile.js';
|
|
32
32
|
import { normalizeEntities, explainMapping } from './model.js';
|
|
33
33
|
import { entityCore } from './entity.js';
|
|
34
|
-
import { createTracker } from './tracker.js';
|
|
34
|
+
import { createTracker, membershipKeys } from './tracker.js';
|
|
35
35
|
import { createCaptureEngine, DEFAULT_RETENTION } from './capture.js';
|
|
36
36
|
import { createLiveRegistry, classifyLiveQuery, LIVE_DEFAULTS } from './live.js';
|
|
37
37
|
import { normalizeEventTime } from './live-time.js';
|
|
38
38
|
import { createJobEngine } from './jobs.js';
|
|
39
|
-
import { collectEntityRoots } from './plan.js';
|
|
39
|
+
import { collectEntityRoots, entityRoot } from './plan.js';
|
|
40
40
|
import {
|
|
41
41
|
DERIVE_KINDS, PHYSICAL_KINDS, PRECISION_MIN, PRECISION_MAX, DIMS_MIN, DIMS_MAX,
|
|
42
42
|
derivedValue, memberAt, storedMemberForm, registerDeriveFunctions,
|
|
@@ -369,13 +369,6 @@ function requireKey(key, collection, docPath) {
|
|
|
369
369
|
* @param {string} keyColumn
|
|
370
370
|
* @returns {boolean}
|
|
371
371
|
*/
|
|
372
|
-
function isDuplicateKey(error, table, keyColumn) {
|
|
373
|
-
if (error?.errcode === 1555) return true;
|
|
374
|
-
return typeof error?.message === 'string'
|
|
375
|
-
&& error.message.includes('UNIQUE constraint failed')
|
|
376
|
-
&& error.message.includes(`${table}.${keyColumn}`);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
372
|
/**
|
|
380
373
|
* Wrap a database failure for one collection operation.
|
|
381
374
|
* @param {any} error
|
|
@@ -386,7 +379,10 @@ function isDuplicateKey(error, table, keyColumn) {
|
|
|
386
379
|
* @returns {DbRuntimeError}
|
|
387
380
|
*/
|
|
388
381
|
function wrapWriteError(error, plan, collection, docPath, key) {
|
|
389
|
-
|
|
382
|
+
// an error that already carries a code (a closed store, a refused
|
|
383
|
+
// document) is the error; only the driver's own failures are wrapped
|
|
384
|
+
if (typeof error?.code === 'string' && error.code.startsWith('JD')) return error;
|
|
385
|
+
if (isDuplicateKeyError(error, plan.table, plan.keyColumn)) {
|
|
390
386
|
return new DbRuntimeError('JD2001',
|
|
391
387
|
`a document already exists under key '${String(key)}'`,
|
|
392
388
|
{ docPath, collection, key, cause: error });
|
|
@@ -594,16 +590,9 @@ function collectionCore(connection, collection, plan, validate, queryState, stor
|
|
|
594
590
|
};
|
|
595
591
|
|
|
596
592
|
const runWrite = (statementName, sql, params, key, reads) => {
|
|
597
|
-
return chain(prepared(statementName, sql), (statement) =>
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
out = reads ? statement.get(params) : statement.run(params);
|
|
601
|
-
}
|
|
602
|
-
catch (error) {
|
|
603
|
-
throw wrapWriteError(error, plan, collection.name, collection.docPath, key);
|
|
604
|
-
}
|
|
605
|
-
return out;
|
|
606
|
-
});
|
|
593
|
+
return chain(prepared(statementName, sql), (statement) =>
|
|
594
|
+
attempt(() => (reads ? statement.get(params) : statement.run(params)),
|
|
595
|
+
(error) => wrapWriteError(error, plan, collection.name, collection.docPath, key)));
|
|
607
596
|
};
|
|
608
597
|
|
|
609
598
|
const core = {
|
|
@@ -673,15 +662,10 @@ function collectionCore(connection, collection, plan, validate, queryState, stor
|
|
|
673
662
|
const { expression, params } = translated.build(
|
|
674
663
|
dialect.quoteIdentifier(plan.docColumn), 1);
|
|
675
664
|
const sql = dialect.dml.updateDoc(shape, expression, params.length + 1);
|
|
676
|
-
return chain(prepared(`patch:${sql}`, sql), (statement) =>
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
catch (error) {
|
|
681
|
-
throw wrapWriteError(error, plan, collection.name, collection.docPath, key);
|
|
682
|
-
}
|
|
683
|
-
return next;
|
|
684
|
-
});
|
|
665
|
+
return chain(prepared(`patch:${sql}`, sql), (statement) =>
|
|
666
|
+
chain(attempt(() => statement.run([...params, ...derivedFor(next), key]),
|
|
667
|
+
(error) => wrapWriteError(error, plan, collection.name, collection.docPath, key)),
|
|
668
|
+
() => next));
|
|
685
669
|
});
|
|
686
670
|
},
|
|
687
671
|
delete(key) {
|
|
@@ -756,6 +740,26 @@ function asyncCollection(core, live) {
|
|
|
756
740
|
* @param {any} options
|
|
757
741
|
* @returns {{ functions: any, extensions: any } | null}
|
|
758
742
|
*/
|
|
743
|
+
/**
|
|
744
|
+
* The schema a WRITE validates against. A store-allocated key (`default:
|
|
745
|
+
* "auto"`) is absent from the document the injected hook sees — the
|
|
746
|
+
* database allocates it after validation — so it cannot be required of a
|
|
747
|
+
* write, and the generated input type already marks it optional; every
|
|
748
|
+
* other member is the schema's own, defaults filled (§9.6). The read
|
|
749
|
+
* shape is untouched: the document the store answers carries the key.
|
|
750
|
+
* @param {any} entity - a normalized entity
|
|
751
|
+
* @returns {any}
|
|
752
|
+
*/
|
|
753
|
+
function writeSchemaOf(entity) {
|
|
754
|
+
const schema = entity.schema;
|
|
755
|
+
const auto = entity.keys.find((key) => entity.properties.get(key).default === 'auto');
|
|
756
|
+
if (auto === undefined || !Array.isArray(schema?.required) || !schema.required.includes(auto))
|
|
757
|
+
return schema;
|
|
758
|
+
const out = { ...schema, required: schema.required.filter((name) => name !== auto) };
|
|
759
|
+
if (out.required.length === 0) delete out.required;
|
|
760
|
+
return out;
|
|
761
|
+
}
|
|
762
|
+
|
|
759
763
|
function resolveOperators(options) {
|
|
760
764
|
const registry = options.operators;
|
|
761
765
|
const hasRegistry = registry !== undefined && registry !== null;
|
|
@@ -1114,8 +1118,9 @@ export function openStore(model, options) {
|
|
|
1114
1118
|
});
|
|
1115
1119
|
}
|
|
1116
1120
|
for (const joinName of Object.keys(mapping?.joinTables ?? {})) {
|
|
1117
|
-
const
|
|
1118
|
-
const columns =
|
|
1121
|
+
const join = mapping.joinTables[joinName];
|
|
1122
|
+
const columns = [join.left, join.right]
|
|
1123
|
+
.map((side) => ({ name: side.column, role: 'key' }));
|
|
1119
1124
|
captureShapes.set(joinName, {
|
|
1120
1125
|
kind: 'join', columns,
|
|
1121
1126
|
keyIndexes: columns.map((_, i) => i), docIndex: -1,
|
|
@@ -1155,7 +1160,15 @@ export function openStore(model, options) {
|
|
|
1155
1160
|
defaults: typeof options.jobs === 'object' ? options.jobs : undefined,
|
|
1156
1161
|
});
|
|
1157
1162
|
/** Register a collection live query (LIVE-FORMAT §7). */
|
|
1163
|
+
const refuseAsyncLive = () => {
|
|
1164
|
+
if (connection.synchronous !== true) {
|
|
1165
|
+
throw new DbCompileError('JD0051',
|
|
1166
|
+
'live queries are not maintained over an asynchronous connection — a '
|
|
1167
|
+
+ 'synchronous driver (node, bun, a synchronous wasm handle) keeps them');
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1158
1170
|
const registerCollectionLive = (core, document, liveOptions) => {
|
|
1171
|
+
refuseAsyncLive();
|
|
1159
1172
|
const externals = liveOptions?.externals ?? {};
|
|
1160
1173
|
const keyed = core.model.keySegments !== null;
|
|
1161
1174
|
const eventTime = normalizeEventTime(liveOptions, core.model.name);
|
|
@@ -1193,26 +1206,39 @@ export function openStore(model, options) {
|
|
|
1193
1206
|
const captureJoinDelete = capture === null || capture.mode !== 'journal'
|
|
1194
1207
|
? null
|
|
1195
1208
|
: (entityName, keyParts) => {
|
|
1196
|
-
const joins = Object.
|
|
1197
|
-
.filter((
|
|
1209
|
+
const joins = Object.entries(mapping?.joinTables ?? {})
|
|
1210
|
+
.filter(([, join]) => join.left.entity === entityName
|
|
1211
|
+
|| join.right.entity === entityName);
|
|
1198
1212
|
const nextJoin = (i) => {
|
|
1199
1213
|
if (i >= joins.length) return null;
|
|
1200
|
-
const joinName = joins[i];
|
|
1201
|
-
const
|
|
1202
|
-
const
|
|
1214
|
+
const [joinName, join] = joins[i];
|
|
1215
|
+
const columns = [join.left.column, join.right.column];
|
|
1216
|
+
const own = join.left.entity === entityName ? join.left : join.right;
|
|
1217
|
+
const sql = `SELECT ${columns.map(dialect.quoteIdentifier).join(', ')} `
|
|
1203
1218
|
+ `FROM ${dialect.quoteIdentifier(joinName)} `
|
|
1204
|
-
+ `WHERE ${dialect.quoteIdentifier(
|
|
1219
|
+
+ `WHERE ${dialect.quoteIdentifier(own.column)} = ${dialect.parameterRef(1, 'v')}`;
|
|
1205
1220
|
return chain(connection.prepare(sql), (statement) =>
|
|
1206
1221
|
chain(statement.all([keyParts[0]]), (rows) => {
|
|
1207
1222
|
for (const row of rows) {
|
|
1208
|
-
capture.record(joinName,
|
|
1209
|
-
pair.map((part) => row[`${part}_key`]), undefined, null);
|
|
1223
|
+
capture.record(joinName, columns.map((column) => row[column]), undefined, null);
|
|
1210
1224
|
}
|
|
1211
1225
|
return nextJoin(i + 1);
|
|
1212
1226
|
}));
|
|
1213
1227
|
};
|
|
1214
1228
|
return nextJoin(0);
|
|
1215
1229
|
};
|
|
1230
|
+
/** The document a keyed `put` replaces, for the journal's
|
|
1231
|
+
* before-image: resolved through the collection's own key when
|
|
1232
|
+
* the caller passed none — a plain upsert recorded as an
|
|
1233
|
+
* `add` of the whole document, and a no-op put as a record. */
|
|
1234
|
+
const readBefore = (core, doc, key) => {
|
|
1235
|
+
const resolved = key !== undefined ? key
|
|
1236
|
+
: core.model.keySegments !== null
|
|
1237
|
+
? extractKey(doc, core.model.keySegments, core.model.key,
|
|
1238
|
+
core.model.name, core.model.docPath)
|
|
1239
|
+
: undefined;
|
|
1240
|
+
return resolved === undefined ? undefined : core.get(resolved);
|
|
1241
|
+
};
|
|
1216
1242
|
/** Journal-mode write wrappers for a collection core. */
|
|
1217
1243
|
const captureCollection = (collectionName, core) => {
|
|
1218
1244
|
if (capture === null) return core;
|
|
@@ -1224,7 +1250,7 @@ export function openStore(model, options) {
|
|
|
1224
1250
|
return key;
|
|
1225
1251
|
})),
|
|
1226
1252
|
put: (doc, key) => guard(() => (journal
|
|
1227
|
-
? chain(
|
|
1253
|
+
? chain(readBefore(core, doc, key), (before) =>
|
|
1228
1254
|
chain(core.put(doc, key), (storedKey) => {
|
|
1229
1255
|
capture.record(collectionName, [storedKey], before ?? null, doc);
|
|
1230
1256
|
return storedKey;
|
|
@@ -1312,7 +1338,9 @@ export function openStore(model, options) {
|
|
|
1312
1338
|
captureLog: captureMode !== 'none'
|
|
1313
1339
|
&& (captureRequested.log === true
|
|
1314
1340
|
|| (captureRequested.log !== undefined && captureRequested.log !== false)),
|
|
1315
|
-
|
|
1341
|
+
// maintenance reads rows synchronously; an asynchronous
|
|
1342
|
+
// connection is never maintained (LIVE-FORMAT §12) and says so
|
|
1343
|
+
live: captureMode !== 'none' && connection.synchronous === true,
|
|
1316
1344
|
jobs: options.jobs === true
|
|
1317
1345
|
|| (options.jobs !== undefined && options.jobs !== false),
|
|
1318
1346
|
});
|
|
@@ -1350,8 +1378,10 @@ export function openStore(model, options) {
|
|
|
1350
1378
|
{ docPath: '/entities', collection: name });
|
|
1351
1379
|
}
|
|
1352
1380
|
const validate = options.compileSchema !== undefined
|
|
1353
|
-
? options.compileSchema(entity
|
|
1381
|
+
? options.compileSchema(writeSchemaOf(entity))
|
|
1354
1382
|
: null;
|
|
1383
|
+
if (validate !== null && typeof validate !== 'function')
|
|
1384
|
+
throw new TypeError('openStore: compileSchema must return a validation function');
|
|
1355
1385
|
core = captureEntity(name, entityCore(connection, entity,
|
|
1356
1386
|
mapping.entities[name], validate));
|
|
1357
1387
|
entityCores.set(name, core);
|
|
@@ -1381,9 +1411,78 @@ export function openStore(model, options) {
|
|
|
1381
1411
|
if (ops !== undefined) return ops;
|
|
1382
1412
|
const core = entityCoreFor(name);
|
|
1383
1413
|
const loads = loadEngineFor(name);
|
|
1414
|
+
/**
|
|
1415
|
+
* The many-to-many memberships a document carries, as join
|
|
1416
|
+
* rows: `create()` attaches them after the insert, in the
|
|
1417
|
+
* same transaction — the `<Name>Input` type and `add()` say
|
|
1418
|
+
* a membership array is writable, and `create()` refusing it
|
|
1419
|
+
* made the generated type a lie.
|
|
1420
|
+
* @param {any} doc
|
|
1421
|
+
*/
|
|
1422
|
+
const membershipsOf = (doc) => {
|
|
1423
|
+
const out = [];
|
|
1424
|
+
for (const property of entities.get(name).properties.values()) {
|
|
1425
|
+
const relation = property.relation;
|
|
1426
|
+
if (relation?.kind !== 'manyToMany') continue;
|
|
1427
|
+
const join = mapping.joinTables[relation.joinTable];
|
|
1428
|
+
const own = join.left.entity === name ? join.left : join.right;
|
|
1429
|
+
const target = own === join.left ? join.right : join.left;
|
|
1430
|
+
const keys = [...new Set(membershipKeys(doc?.[property.name],
|
|
1431
|
+
target.referencesKey, property.name,
|
|
1432
|
+
(reason) => new DbRuntimeError('JD2003', reason,
|
|
1433
|
+
{ docPath: entities.get(name).docPath, collection: name })))];
|
|
1434
|
+
if (keys.length > 0) out.push({ table: relation.joinTable, join, own, target, keys });
|
|
1435
|
+
}
|
|
1436
|
+
return out;
|
|
1437
|
+
};
|
|
1438
|
+
const attach = (made, memberships) => {
|
|
1439
|
+
const ownKey = made[mapping.entities[name].keys[0]];
|
|
1440
|
+
const next = (i) => {
|
|
1441
|
+
if (i >= memberships.length) return null;
|
|
1442
|
+
const { table, join, own, target, keys } = memberships[i];
|
|
1443
|
+
const sql = `INSERT INTO ${dialect.quoteIdentifier(table)} `
|
|
1444
|
+
+ `(${dialect.quoteIdentifier(own.column)}, ${dialect.quoteIdentifier(target.column)}) `
|
|
1445
|
+
+ `VALUES (${dialect.parameterRef(1, 'v')}, ${dialect.parameterRef(2, 'v')})`;
|
|
1446
|
+
return chain(connection.prepare(sql), (statement) => {
|
|
1447
|
+
const row = (j) => {
|
|
1448
|
+
if (j >= keys.length) return next(i + 1);
|
|
1449
|
+
let ran;
|
|
1450
|
+
try {
|
|
1451
|
+
ran = statement.run([ownKey, keys[j]]);
|
|
1452
|
+
}
|
|
1453
|
+
catch (error) {
|
|
1454
|
+
throw new DbRuntimeError('JD2005',
|
|
1455
|
+
`the database rejected the operation: ${/** @type {any} */ (error)?.message ?? String(error)}`,
|
|
1456
|
+
{ docPath: entities.get(name).docPath, collection: name, key: ownKey, cause: error });
|
|
1457
|
+
}
|
|
1458
|
+
return chain(ran, () => {
|
|
1459
|
+
if (capture !== null && capture.mode === 'journal') {
|
|
1460
|
+
const value = { [own.column]: ownKey, [target.column]: keys[j] };
|
|
1461
|
+
const ordered = {};
|
|
1462
|
+
for (const column of [join.left.column, join.right.column])
|
|
1463
|
+
ordered[column] = value[column];
|
|
1464
|
+
capture.record(table, Object.values(ordered), null, ordered);
|
|
1465
|
+
}
|
|
1466
|
+
return row(j + 1);
|
|
1467
|
+
});
|
|
1468
|
+
};
|
|
1469
|
+
return row(0);
|
|
1470
|
+
});
|
|
1471
|
+
};
|
|
1472
|
+
return next(0);
|
|
1473
|
+
};
|
|
1384
1474
|
ops = {
|
|
1385
|
-
create: (doc) =>
|
|
1386
|
-
|
|
1475
|
+
create: (doc) => {
|
|
1476
|
+
const memberships = membershipsOf(doc);
|
|
1477
|
+
if (memberships.length === 0)
|
|
1478
|
+
return chain(core.create(doc), (made) => tracker.register(name, made));
|
|
1479
|
+
// one capture scope and one transaction around the row and
|
|
1480
|
+
// its join rows: a membership the database refuses rolls the
|
|
1481
|
+
// row back too, and journal capture records the join rows
|
|
1482
|
+
return chain(guard(() => connection.transaction(() =>
|
|
1483
|
+
chain(core.create(doc), (made) => chain(attach(made, memberships), () => made)))),
|
|
1484
|
+
(made) => tracker.register(name, made));
|
|
1485
|
+
},
|
|
1387
1486
|
get: (key) => chain(core.get(key), (doc) =>
|
|
1388
1487
|
(doc === undefined ? undefined : tracker.register(name, doc))),
|
|
1389
1488
|
update: (key, changes) => chain(core.update(key, changes),
|
|
@@ -1399,6 +1498,10 @@ export function openStore(model, options) {
|
|
|
1399
1498
|
put: (next) => tracker.put(name, next),
|
|
1400
1499
|
remove: (keyOrDoc) => tracker.remove(name, keyOrDoc),
|
|
1401
1500
|
discard: (keyOrDoc) => tracker.discard(name, keyOrDoc),
|
|
1501
|
+
// membership (§11.7): local bookkeeping like add/put/remove;
|
|
1502
|
+
// the join rows are written by saveChanges()
|
|
1503
|
+
link: (own, member, target) => tracker.link(name, own, member, target),
|
|
1504
|
+
unlink: (own, member, target) => tracker.unlink(name, own, member, target),
|
|
1402
1505
|
noTracking: {
|
|
1403
1506
|
get: (key) => core.get(key),
|
|
1404
1507
|
load: (spec) => loads.load(spec),
|
|
@@ -1449,7 +1552,22 @@ export function openStore(model, options) {
|
|
|
1449
1552
|
put: ops.put,
|
|
1450
1553
|
remove: ops.remove,
|
|
1451
1554
|
discard: ops.discard,
|
|
1555
|
+
link: ops.link,
|
|
1556
|
+
unlink: ops.unlink,
|
|
1452
1557
|
asNoTracking: () => untracked,
|
|
1558
|
+
// the provider contract over ONE entity root (MODEL-FORMAT
|
|
1559
|
+
// §10.1): the document is over the multi-entity root and
|
|
1560
|
+
// goes to the entity engine whole; `root` is the hint a
|
|
1561
|
+
// chain binds its items through, `scope` the identity two
|
|
1562
|
+
// sets of one store share so their documents may be joined
|
|
1563
|
+
// (it carries every root's `relations`, so a hop may chain),
|
|
1564
|
+
// `relations` this entity's own relation table (§10.1).
|
|
1565
|
+
// `execute` stays value-or-promise (D2), as a collection's
|
|
1566
|
+
execute: (document, queryOptions) => entityEngine.execute(document, queryOptions),
|
|
1567
|
+
explain: lift((document, queryOptions) => entityEngine.explain(document, queryOptions)),
|
|
1568
|
+
root: entityRoot(name),
|
|
1569
|
+
scope: entityEngine,
|
|
1570
|
+
relations: entityEngine.relations[name],
|
|
1453
1571
|
});
|
|
1454
1572
|
asyncEntityHandles.set(name, handle);
|
|
1455
1573
|
}
|
|
@@ -1457,11 +1575,15 @@ export function openStore(model, options) {
|
|
|
1457
1575
|
},
|
|
1458
1576
|
saveChanges: entities.size === 0 ? undefined
|
|
1459
1577
|
: lift(() => guard(() => tracker.saveChanges())),
|
|
1460
|
-
// entity DOCUMENTS query the multi-entity root at the store
|
|
1578
|
+
// entity DOCUMENTS query the multi-entity root at the store;
|
|
1579
|
+
// `roots` names the entity arrays this provider serves, so a
|
|
1580
|
+
// chain asked to iterate the store itself can refuse by name
|
|
1461
1581
|
execute: entityEngine === null ? undefined
|
|
1462
1582
|
: (document, queryOptions) => entityEngine.execute(document, queryOptions),
|
|
1463
1583
|
explain: entityEngine === null ? undefined
|
|
1464
1584
|
: lift((document, queryOptions) => entityEngine.explain(document, queryOptions)),
|
|
1585
|
+
roots: entityEngine === null ? undefined : Object.freeze([...entities.keys()]),
|
|
1586
|
+
relations: entityEngine === null ? undefined : entityEngine.relations,
|
|
1465
1587
|
// entity live queries re-run on invalidation — declared,
|
|
1466
1588
|
// not attempted (LIVE-FORMAT §7)
|
|
1467
1589
|
live: entityEngine === null ? undefined
|
|
@@ -1470,6 +1592,7 @@ export function openStore(model, options) {
|
|
|
1470
1592
|
throw new DbCompileError('JD0050',
|
|
1471
1593
|
'live queries require change capture — open the store with { capture: true }');
|
|
1472
1594
|
}
|
|
1595
|
+
refuseAsyncLive();
|
|
1473
1596
|
if (liveOptions?.eventTime !== undefined) {
|
|
1474
1597
|
throw new DbCompileError('JD0053',
|
|
1475
1598
|
'live eventTime maintains a collection view — an entity document re-runs, '
|
|
@@ -1583,6 +1706,8 @@ export function openStore(model, options) {
|
|
|
1583
1706
|
if (connection.synchronous) {
|
|
1584
1707
|
/** @type {Map<string, any>} */
|
|
1585
1708
|
const syncHandles = new Map();
|
|
1709
|
+
/** @type {Map<string, any>} */
|
|
1710
|
+
const syncEntityHandles = new Map();
|
|
1586
1711
|
store.sync = Object.freeze({
|
|
1587
1712
|
collection(name) {
|
|
1588
1713
|
let handle = syncHandles.get(name);
|
|
@@ -1602,31 +1727,61 @@ export function openStore(model, options) {
|
|
|
1602
1727
|
}
|
|
1603
1728
|
return handle;
|
|
1604
1729
|
},
|
|
1605
|
-
transaction: (fn) =>
|
|
1730
|
+
transaction: (fn) => {
|
|
1731
|
+
// the synchronous surface answers values: while a
|
|
1732
|
+
// transaction owns the connection it could only QUEUE,
|
|
1733
|
+
// which handed a Promise back under a value's type
|
|
1734
|
+
if (opened.mustQueue) {
|
|
1735
|
+
throw new DbCompileError('JD0012',
|
|
1736
|
+
'the synchronous transaction cannot wait for the open transaction to '
|
|
1737
|
+
+ 'settle — nest through the store the callback received, or use the '
|
|
1738
|
+
+ 'asynchronous store.transaction()');
|
|
1739
|
+
}
|
|
1740
|
+
return topLevelTransaction(fn);
|
|
1741
|
+
},
|
|
1606
1742
|
entity(name) {
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1743
|
+
let handle = syncEntityHandles.get(name);
|
|
1744
|
+
if (handle === undefined) {
|
|
1745
|
+
const ops = trackedOpsFor(name);
|
|
1746
|
+
const untracked = Object.freeze({
|
|
1747
|
+
get: (key) => ops.noTracking.get(key),
|
|
1748
|
+
load: (spec) => ops.noTracking.load(spec),
|
|
1749
|
+
});
|
|
1750
|
+
handle = Object.freeze({
|
|
1751
|
+
create: (doc) => ops.create(doc),
|
|
1752
|
+
get: (key) => ops.get(key),
|
|
1753
|
+
update: (key, changes) => ops.update(key, changes),
|
|
1754
|
+
delete: (key) => ops.delete(key),
|
|
1755
|
+
load: (spec) => ops.load(spec),
|
|
1756
|
+
explainLoad: ops.explainLoad,
|
|
1757
|
+
add: ops.add,
|
|
1758
|
+
put: ops.put,
|
|
1759
|
+
remove: ops.remove,
|
|
1760
|
+
discard: ops.discard,
|
|
1761
|
+
link: ops.link,
|
|
1762
|
+
unlink: ops.unlink,
|
|
1763
|
+
asNoTracking: () => untracked,
|
|
1764
|
+
// the same provider members as the asynchronous handle,
|
|
1765
|
+
// answering values; one handle per name, so two chains
|
|
1766
|
+
// over one set share one source identity
|
|
1767
|
+
execute: (document, queryOptions) => entityEngine.execute(document, queryOptions),
|
|
1768
|
+
explain: (document, queryOptions) => entityEngine.explain(document, queryOptions),
|
|
1769
|
+
root: entityRoot(name),
|
|
1770
|
+
scope: entityEngine,
|
|
1771
|
+
relations: entityEngine.relations[name],
|
|
1772
|
+
});
|
|
1773
|
+
syncEntityHandles.set(name, handle);
|
|
1774
|
+
}
|
|
1775
|
+
return handle;
|
|
1625
1776
|
},
|
|
1626
1777
|
saveChanges: entities.size === 0 ? undefined
|
|
1627
1778
|
: () => guard(() => tracker.saveChanges()),
|
|
1628
1779
|
execute: entityEngine === null ? undefined
|
|
1629
1780
|
: (document, queryOptions) => entityEngine.execute(document, queryOptions),
|
|
1781
|
+
explain: entityEngine === null ? undefined
|
|
1782
|
+
: (document, queryOptions) => entityEngine.explain(document, queryOptions),
|
|
1783
|
+
roots: entityEngine === null ? undefined : Object.freeze([...entities.keys()]),
|
|
1784
|
+
relations: entityEngine === null ? undefined : entityEngine.relations,
|
|
1630
1785
|
});
|
|
1631
1786
|
}
|
|
1632
1787
|
return chain(capture === null ? null : capture.ready,
|