@mvd/memrel 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +398 -2
- package/dist/index.d.ts +267 -0
- package/dist/index.js +852 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
/** Every error memrel throws on purpose is a `MemrelError`. */
|
|
3
|
+
var MemrelError = class extends Error {
|
|
4
|
+
name = "MemrelError";
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
// src/encode.ts
|
|
12
|
+
/**
|
|
13
|
+
* Key encoding.
|
|
14
|
+
*
|
|
15
|
+
* Every index is a `Map` keyed by an encoded tuple of field values. The encoding keeps value
|
|
16
|
+
* types apart, so `1` and `'1'` never collide, and the same encoder is used for primary keys,
|
|
17
|
+
* unique keys, foreign keys and secondary indexes. That is what lets a child's foreign key tuple
|
|
18
|
+
* be used directly as a lookup into its target's index.
|
|
19
|
+
*/
|
|
20
|
+
const objectIds = /* @__PURE__ */ new WeakMap();
|
|
21
|
+
const symbolIds = /* @__PURE__ */ new Map();
|
|
22
|
+
let nextObjectId = 0;
|
|
23
|
+
/** Encodes one value. Objects other than dates compare by identity. */
|
|
24
|
+
function encodeValue(value) {
|
|
25
|
+
switch (typeof value) {
|
|
26
|
+
case "string": return `s:${value}`;
|
|
27
|
+
case "number": return `n:${value}`;
|
|
28
|
+
case "boolean": return value ? "b:1" : "b:0";
|
|
29
|
+
case "bigint": return `i:${value}`;
|
|
30
|
+
case "undefined": return "u";
|
|
31
|
+
case "symbol": {
|
|
32
|
+
let id = symbolIds.get(value);
|
|
33
|
+
if (id === void 0) {
|
|
34
|
+
id = nextObjectId++;
|
|
35
|
+
symbolIds.set(value, id);
|
|
36
|
+
}
|
|
37
|
+
return `y:${id}`;
|
|
38
|
+
}
|
|
39
|
+
default: {
|
|
40
|
+
if (value === null) return "z";
|
|
41
|
+
if (value instanceof Date) return `d:${value.getTime()}`;
|
|
42
|
+
let id = objectIds.get(value);
|
|
43
|
+
if (id === void 0) {
|
|
44
|
+
id = nextObjectId++;
|
|
45
|
+
objectIds.set(value, id);
|
|
46
|
+
}
|
|
47
|
+
return `o:${id}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Encodes the values of `fields` in `row`, in order. */
|
|
52
|
+
function encodeFields(row, fields) {
|
|
53
|
+
const r = row;
|
|
54
|
+
if (fields.length === 1) return encodeValue(r[fields[0]]);
|
|
55
|
+
let out = "[";
|
|
56
|
+
for (let i = 0; i < fields.length; i++) {
|
|
57
|
+
if (i > 0) out += ",";
|
|
58
|
+
out += JSON.stringify(encodeValue(r[fields[i]]));
|
|
59
|
+
}
|
|
60
|
+
return `${out}]`;
|
|
61
|
+
}
|
|
62
|
+
/** Readable form of a row's key, for error messages: `["u1"]`, `[1,"u1"]`. */
|
|
63
|
+
function displayKey(row, fields) {
|
|
64
|
+
const r = row;
|
|
65
|
+
return `[${fields.map((f) => displayValue(r[f])).join(",")}]`;
|
|
66
|
+
}
|
|
67
|
+
function displayValue(value) {
|
|
68
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
69
|
+
if (typeof value === "symbol") return value.toString();
|
|
70
|
+
if (value instanceof Date) return value.toISOString();
|
|
71
|
+
if (value === void 0) return "undefined";
|
|
72
|
+
try {
|
|
73
|
+
const json = JSON.stringify(value);
|
|
74
|
+
if (json !== void 0) return json;
|
|
75
|
+
} catch {}
|
|
76
|
+
return Object.prototype.toString.call(value);
|
|
77
|
+
}
|
|
78
|
+
// src/model.ts
|
|
79
|
+
/**
|
|
80
|
+
* The runtime model built from a config: one {@link Table} per collection, with its refs, the
|
|
81
|
+
* refs pointing at it, and its mutable index state. The config is validated here, once, when
|
|
82
|
+
* the database is created.
|
|
83
|
+
*/
|
|
84
|
+
const ACTIONS = [
|
|
85
|
+
"cascade",
|
|
86
|
+
"restrict",
|
|
87
|
+
"setNull"
|
|
88
|
+
];
|
|
89
|
+
const COLLECTION_OPTIONS = /* @__PURE__ */ new Set([
|
|
90
|
+
"key",
|
|
91
|
+
"unique",
|
|
92
|
+
"indexes",
|
|
93
|
+
"refs"
|
|
94
|
+
]);
|
|
95
|
+
const REF_OPTIONS = /* @__PURE__ */ new Set([
|
|
96
|
+
"fields",
|
|
97
|
+
"to",
|
|
98
|
+
"references",
|
|
99
|
+
"onDelete",
|
|
100
|
+
"onUpdate",
|
|
101
|
+
"setNullFields",
|
|
102
|
+
"inverse"
|
|
103
|
+
]);
|
|
104
|
+
/** Names used by database-wide methods, which collections can't take. */
|
|
105
|
+
const RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
106
|
+
"load",
|
|
107
|
+
"remove",
|
|
108
|
+
"transaction",
|
|
109
|
+
"snapshot",
|
|
110
|
+
"restore"
|
|
111
|
+
]);
|
|
112
|
+
function fail(message) {
|
|
113
|
+
throw new MemrelError("INVALID_SCHEMA", message);
|
|
114
|
+
}
|
|
115
|
+
function isRecord(value) {
|
|
116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
117
|
+
}
|
|
118
|
+
function fieldList(value, where) {
|
|
119
|
+
if (!Array.isArray(value) || value.length === 0) fail(`${where} must be a non-empty array of field names`);
|
|
120
|
+
for (const f of value) if (typeof f !== "string") fail(`${where} must only contain field names`);
|
|
121
|
+
if (new Set(value).size !== value.length) fail(`${where} contains a field twice`);
|
|
122
|
+
return [...value];
|
|
123
|
+
}
|
|
124
|
+
function sameList(a, b) {
|
|
125
|
+
return a.length === b.length && a.every((f, i) => f === b[i]);
|
|
126
|
+
}
|
|
127
|
+
function sameSet(a, b) {
|
|
128
|
+
return a.length === b.length && a.every((f) => b.includes(f));
|
|
129
|
+
}
|
|
130
|
+
/** Creates empty state for a table. */
|
|
131
|
+
function emptyState(table) {
|
|
132
|
+
return {
|
|
133
|
+
rows: /* @__PURE__ */ new Map(),
|
|
134
|
+
uniques: table.uniques.map(() => /* @__PURE__ */ new Map()),
|
|
135
|
+
indexes: table.indexes.map(() => /* @__PURE__ */ new Map()),
|
|
136
|
+
reverse: table.refs.map(() => /* @__PURE__ */ new Map()),
|
|
137
|
+
seq: 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** Validates a config and builds the runtime model. */
|
|
141
|
+
function buildModel(config) {
|
|
142
|
+
if (!isRecord(config)) fail("config must be an object of collections");
|
|
143
|
+
const tables = /* @__PURE__ */ new Map();
|
|
144
|
+
for (const [name, raw] of Object.entries(config)) {
|
|
145
|
+
if (RESERVED_NAMES.has(name)) fail(`'${name}' is reserved and can't be a collection name`);
|
|
146
|
+
if (!isRecord(raw)) fail(`${name}: config must be an object`);
|
|
147
|
+
for (const option of Object.keys(raw)) if (!COLLECTION_OPTIONS.has(option)) fail(`${name}: unknown option '${option}'`);
|
|
148
|
+
const key = fieldList(raw.key, `${name}.key`);
|
|
149
|
+
const lists = (value, label) => {
|
|
150
|
+
if (value === void 0) return [];
|
|
151
|
+
if (!Array.isArray(value)) fail(`${name}.${label} must be an array of field lists`);
|
|
152
|
+
return value.map((v, i) => fieldList(v, `${name}.${label}[${i}]`));
|
|
153
|
+
};
|
|
154
|
+
tables.set(name, {
|
|
155
|
+
name,
|
|
156
|
+
key,
|
|
157
|
+
uniques: lists(raw.unique, "unique").map((fields, index) => ({
|
|
158
|
+
fields,
|
|
159
|
+
index
|
|
160
|
+
})),
|
|
161
|
+
indexes: lists(raw.indexes, "indexes").map((fields, index) => ({
|
|
162
|
+
fields,
|
|
163
|
+
index
|
|
164
|
+
})),
|
|
165
|
+
refs: [],
|
|
166
|
+
refsByName: /* @__PURE__ */ new Map(),
|
|
167
|
+
incoming: [],
|
|
168
|
+
inverses: /* @__PURE__ */ new Map(),
|
|
169
|
+
state: void 0
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
for (const [name, raw] of Object.entries(config)) {
|
|
173
|
+
const from = tables.get(name);
|
|
174
|
+
if (raw.refs === void 0) continue;
|
|
175
|
+
if (!isRecord(raw.refs)) fail(`${name}.refs must be an object of refs`);
|
|
176
|
+
for (const [refName, r] of Object.entries(raw.refs)) {
|
|
177
|
+
const id = `${name}.${refName}`;
|
|
178
|
+
if (!isRecord(r)) fail(`${id}: ref must be an object`);
|
|
179
|
+
for (const option of Object.keys(r)) if (!REF_OPTIONS.has(option)) fail(`${id}: unknown option '${option}'`);
|
|
180
|
+
const fields = fieldList(r.fields, `${id}.fields`);
|
|
181
|
+
const to = typeof r.to === "string" ? tables.get(r.to) : void 0;
|
|
182
|
+
if (!to) fail(`${id}.to must name a collection`);
|
|
183
|
+
let references;
|
|
184
|
+
if (r.references === void 0) {
|
|
185
|
+
if (fields.length !== 1 || to.key.length !== 1) fail(`${id}: composite foreign keys need explicit 'references'`);
|
|
186
|
+
references = [...to.key];
|
|
187
|
+
} else references = fieldList(r.references, `${id}.references`);
|
|
188
|
+
if (references.length !== fields.length) fail(`${id}: 'fields' and 'references' must have the same length`);
|
|
189
|
+
let targetUnique = null;
|
|
190
|
+
if (!sameList(references, to.key)) {
|
|
191
|
+
targetUnique = to.uniques.find((u) => sameList(u.fields, references)) ?? null;
|
|
192
|
+
if (!targetUnique) fail(`${id}: 'references' must be the key or a unique key of '${to.name}', in order`);
|
|
193
|
+
}
|
|
194
|
+
const onDelete = r.onDelete;
|
|
195
|
+
if (!ACTIONS.includes(onDelete)) fail(`${id}.onDelete must be one of ${ACTIONS.join(", ")}`);
|
|
196
|
+
const onUpdate = r.onUpdate ?? "restrict";
|
|
197
|
+
if (!ACTIONS.includes(onUpdate)) fail(`${id}.onUpdate must be one of ${ACTIONS.join(", ")}`);
|
|
198
|
+
const nullFields = r.setNullFields === void 0 ? fields : fieldList(r.setNullFields, `${id}.setNullFields`);
|
|
199
|
+
if (!nullFields.every((f) => fields.includes(f))) fail(`${id}: 'setNullFields' must be a subset of 'fields'`);
|
|
200
|
+
if (r.inverse !== void 0 && typeof r.inverse !== "string") fail(`${id}.inverse must be a string`);
|
|
201
|
+
const ref = {
|
|
202
|
+
name: refName,
|
|
203
|
+
id,
|
|
204
|
+
index: from.refs.length,
|
|
205
|
+
from,
|
|
206
|
+
to,
|
|
207
|
+
fields,
|
|
208
|
+
references,
|
|
209
|
+
targetUnique,
|
|
210
|
+
onDelete,
|
|
211
|
+
onUpdate,
|
|
212
|
+
nullFields,
|
|
213
|
+
inverse: r.inverse,
|
|
214
|
+
inverseSingle: sameSet(fields, from.key) || from.uniques.some((u) => sameSet(u.fields, fields))
|
|
215
|
+
};
|
|
216
|
+
from.refs.push(ref);
|
|
217
|
+
from.refsByName.set(refName, ref);
|
|
218
|
+
to.incoming.push(ref);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const table of tables.values()) {
|
|
222
|
+
for (const ref of table.refs) {
|
|
223
|
+
if (ref.inverse === void 0) continue;
|
|
224
|
+
const target = ref.to;
|
|
225
|
+
if (target.refsByName.has(ref.inverse) || target.inverses.has(ref.inverse)) fail(`${ref.id}: inverse '${ref.inverse}' is already a relation of '${target.name}'`);
|
|
226
|
+
target.inverses.set(ref.inverse, ref);
|
|
227
|
+
}
|
|
228
|
+
for (const ref of table.refs) {
|
|
229
|
+
if (ref.onDelete !== "setNull" && ref.onUpdate !== "setNull") continue;
|
|
230
|
+
for (const other of table.refs) if (other !== ref && other.fields.some((f) => ref.nullFields.includes(f))) fail(`${ref.id}: setNull would clear a column ${other.id} shares; use 'setNullFields' to clear only the others`);
|
|
231
|
+
}
|
|
232
|
+
table.state = emptyState(table);
|
|
233
|
+
}
|
|
234
|
+
return tables;
|
|
235
|
+
}
|
|
236
|
+
// src/store.ts
|
|
237
|
+
/**
|
|
238
|
+
* The engine: storage, index maintenance, reads, writes, the undo log and snapshots.
|
|
239
|
+
*
|
|
240
|
+
* Every write runs inside a savepoint of the undo log. Each primitive change (store a row,
|
|
241
|
+
* remove a row, add to or remove from an index) appends its inverse, so a write that throws
|
|
242
|
+
* midway is rolled back and the store is unchanged. Integrity checks run at the end of each
|
|
243
|
+
* write against the final state, which is what lets a batch reference rows later in the same
|
|
244
|
+
* batch, and a delete remove a row together with the rows `restrict`-ing it.
|
|
245
|
+
*/
|
|
246
|
+
const MAX_CASCADE_DEPTH = 1e3;
|
|
247
|
+
function bySeq(a, b) {
|
|
248
|
+
return a.seq - b.seq;
|
|
249
|
+
}
|
|
250
|
+
/** Sorts entries into insertion order, skipping the sort when they already are. */
|
|
251
|
+
function inOrder(entries) {
|
|
252
|
+
for (let i = 1; i < entries.length; i++) if (entries[i - 1].seq > entries[i].seq) return entries.sort(bySeq);
|
|
253
|
+
return entries;
|
|
254
|
+
}
|
|
255
|
+
function isInCondition(value) {
|
|
256
|
+
if (typeof value !== "object" || value === null) return false;
|
|
257
|
+
const proto = Object.getPrototypeOf(value);
|
|
258
|
+
if (proto !== Object.prototype && proto !== null) return false;
|
|
259
|
+
const keys = Object.keys(value);
|
|
260
|
+
return keys.length === 1 && keys[0] === "in" && Array.isArray(value.in);
|
|
261
|
+
}
|
|
262
|
+
/** Every combination of one encoded value per field, encoded as a tuple. */
|
|
263
|
+
function combine(parts) {
|
|
264
|
+
if (parts.length === 1) return [...parts[0]];
|
|
265
|
+
let out = [[]];
|
|
266
|
+
for (const part of parts) {
|
|
267
|
+
const next = [];
|
|
268
|
+
for (const prefix of out) for (const v of part) next.push([...prefix, v]);
|
|
269
|
+
out = next;
|
|
270
|
+
}
|
|
271
|
+
return out.map((tuple) => `[${tuple.map((v) => JSON.stringify(v)).join(",")}]`);
|
|
272
|
+
}
|
|
273
|
+
function cloneBuckets(map) {
|
|
274
|
+
const out = /* @__PURE__ */ new Map();
|
|
275
|
+
for (const [k, s] of map) out.set(k, new Set(s));
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
function cloneState(state) {
|
|
279
|
+
return {
|
|
280
|
+
rows: new Map(state.rows),
|
|
281
|
+
uniques: state.uniques.map((m) => new Map(m)),
|
|
282
|
+
indexes: state.indexes.map(cloneBuckets),
|
|
283
|
+
reverse: state.reverse.map(cloneBuckets),
|
|
284
|
+
seq: state.seq
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function isNullish(value) {
|
|
288
|
+
return value === null || value === void 0;
|
|
289
|
+
}
|
|
290
|
+
function push(target, name, row) {
|
|
291
|
+
const list = target[name];
|
|
292
|
+
if (list) list.push(row);
|
|
293
|
+
else target[name] = [row];
|
|
294
|
+
}
|
|
295
|
+
var Store = class {
|
|
296
|
+
tables;
|
|
297
|
+
paths = /* @__PURE__ */ new Map();
|
|
298
|
+
journal = null;
|
|
299
|
+
constructor(config) {
|
|
300
|
+
this.tables = buildModel(config);
|
|
301
|
+
for (const table of this.tables.values()) {
|
|
302
|
+
const paths = [{
|
|
303
|
+
fields: table.key,
|
|
304
|
+
skipsNulls: false,
|
|
305
|
+
lookup: (s, k) => s.rows.has(k) ? k : void 0
|
|
306
|
+
}];
|
|
307
|
+
for (const u of table.uniques) paths.push({
|
|
308
|
+
fields: u.fields,
|
|
309
|
+
skipsNulls: true,
|
|
310
|
+
lookup: (s, k) => s.uniques[u.index].get(k)
|
|
311
|
+
});
|
|
312
|
+
for (const ref of table.refs) paths.push({
|
|
313
|
+
fields: ref.fields,
|
|
314
|
+
skipsNulls: true,
|
|
315
|
+
lookup: (s, k) => s.reverse[ref.index].get(k)
|
|
316
|
+
});
|
|
317
|
+
for (const idx of table.indexes) paths.push({
|
|
318
|
+
fields: idx.fields,
|
|
319
|
+
skipsNulls: false,
|
|
320
|
+
lookup: (s, k) => s.indexes[idx.index].get(k)
|
|
321
|
+
});
|
|
322
|
+
this.paths.set(table, paths);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** Runs `fn`; if it throws, undoes everything it changed. Nests. */
|
|
326
|
+
savepoint(fn) {
|
|
327
|
+
const outer = this.journal;
|
|
328
|
+
const journal = outer ?? [];
|
|
329
|
+
const mark = journal.length;
|
|
330
|
+
this.journal = journal;
|
|
331
|
+
try {
|
|
332
|
+
const result = fn();
|
|
333
|
+
if (typeof result === "object" && result !== null && typeof result.then === "function") throw new MemrelError("INVALID_ARGUMENT", "Transaction callbacks must be synchronous");
|
|
334
|
+
return result;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
while (journal.length > mark) journal.pop()();
|
|
337
|
+
throw error;
|
|
338
|
+
} finally {
|
|
339
|
+
this.journal = outer;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
log(undo) {
|
|
343
|
+
this.journal?.push(undo);
|
|
344
|
+
}
|
|
345
|
+
mapSet(map, key, value) {
|
|
346
|
+
if (map.has(key)) {
|
|
347
|
+
const prev = map.get(key);
|
|
348
|
+
this.log(() => map.set(key, prev));
|
|
349
|
+
} else this.log(() => map.delete(key));
|
|
350
|
+
map.set(key, value);
|
|
351
|
+
}
|
|
352
|
+
mapDelete(map, key) {
|
|
353
|
+
if (!map.has(key)) return;
|
|
354
|
+
const prev = map.get(key);
|
|
355
|
+
map.delete(key);
|
|
356
|
+
this.log(() => map.set(key, prev));
|
|
357
|
+
}
|
|
358
|
+
bucketAdd(map, key, member) {
|
|
359
|
+
let bucket = map.get(key);
|
|
360
|
+
if (!bucket) {
|
|
361
|
+
bucket = /* @__PURE__ */ new Set();
|
|
362
|
+
this.mapSet(map, key, bucket);
|
|
363
|
+
}
|
|
364
|
+
if (bucket.has(member)) return;
|
|
365
|
+
bucket.add(member);
|
|
366
|
+
const b = bucket;
|
|
367
|
+
this.log(() => b.delete(member));
|
|
368
|
+
}
|
|
369
|
+
bucketDelete(map, key, member) {
|
|
370
|
+
const bucket = map.get(key);
|
|
371
|
+
if (!bucket?.delete(member)) return;
|
|
372
|
+
this.log(() => bucket.add(member));
|
|
373
|
+
if (bucket.size === 0) this.mapDelete(map, key);
|
|
374
|
+
}
|
|
375
|
+
/** Runs one write: rolled back as a whole, and checked once complete. */
|
|
376
|
+
write(fn) {
|
|
377
|
+
return this.savepoint(() => {
|
|
378
|
+
const op = {
|
|
379
|
+
touched: [],
|
|
380
|
+
vacated: []
|
|
381
|
+
};
|
|
382
|
+
const result = fn(op);
|
|
383
|
+
this.verify(op);
|
|
384
|
+
return result;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The encoded foreign key of `row` for `ref`: `null` when it references nothing (all of the
|
|
389
|
+
* ref's null fields are null), `undefined` when it is partly null, which is an error.
|
|
390
|
+
*/
|
|
391
|
+
foreignKey(ref, row) {
|
|
392
|
+
if (ref.nullFields.every((f) => isNullish(row[f]))) return null;
|
|
393
|
+
if (ref.fields.some((f) => isNullish(row[f]))) return void 0;
|
|
394
|
+
return encodeFields(row, ref.fields);
|
|
395
|
+
}
|
|
396
|
+
/** The encoded values of `row` that `ref` points at, or `null` when they aren't indexed. */
|
|
397
|
+
referencedValue(ref, row) {
|
|
398
|
+
if (ref.targetUnique && ref.references.some((f) => isNullish(row[f]))) return null;
|
|
399
|
+
return encodeFields(row, ref.references);
|
|
400
|
+
}
|
|
401
|
+
findTarget(ref, value) {
|
|
402
|
+
const state = ref.to.state;
|
|
403
|
+
if (!ref.targetUnique) return state.rows.get(value);
|
|
404
|
+
const pk = state.uniques[ref.targetUnique.index].get(value);
|
|
405
|
+
return pk === void 0 ? void 0 : state.rows.get(pk);
|
|
406
|
+
}
|
|
407
|
+
describe(table, row) {
|
|
408
|
+
return `${table.name} ${displayKey(row, table.key)}`;
|
|
409
|
+
}
|
|
410
|
+
verify(op) {
|
|
411
|
+
for (const v of op.vacated) {
|
|
412
|
+
const children = v.ref.from.state.reverse[v.ref.index].get(v.value);
|
|
413
|
+
if (!children || children.size === 0 || this.findTarget(v.ref, v.value)) continue;
|
|
414
|
+
const n = children.size;
|
|
415
|
+
throw new MemrelError("RESTRICT_VIOLATION", `Cannot ${v.verb} ${this.describe(v.ref.to, v.row)}: restricted by ${n} ${n === 1 ? "row" : "rows"} in ${v.ref.id}`);
|
|
416
|
+
}
|
|
417
|
+
for (const [table, pk] of op.touched) {
|
|
418
|
+
const entry = table.state.rows.get(pk);
|
|
419
|
+
if (entry) this.checkRefs(table, entry.row);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
checkRefs(table, row) {
|
|
423
|
+
for (const ref of table.refs) {
|
|
424
|
+
const fk = this.foreignKey(ref, row);
|
|
425
|
+
if (fk === null) continue;
|
|
426
|
+
if (fk === void 0) throw new MemrelError("FOREIGN_KEY_VIOLATION", `${this.describe(table, row)}: foreign key ${ref.id} ${displayKey(row, ref.fields)} is partly null`);
|
|
427
|
+
if (!this.findTarget(ref, fk)) throw new MemrelError("FOREIGN_KEY_VIOLATION", `${this.describe(table, row)}: ${ref.id} references missing ${ref.to.name} ${displayKey(row, ref.fields)}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
uniqueValue(row, fields) {
|
|
431
|
+
return fields.some((f) => isNullish(row[f])) ? null : encodeFields(row, fields);
|
|
432
|
+
}
|
|
433
|
+
addRow(op, table, pk, row, seq) {
|
|
434
|
+
const s = table.state;
|
|
435
|
+
for (const u of table.uniques) {
|
|
436
|
+
const value = this.uniqueValue(row, u.fields);
|
|
437
|
+
if (value === null) continue;
|
|
438
|
+
const map = s.uniques[u.index];
|
|
439
|
+
const holder = map.get(value);
|
|
440
|
+
if (holder !== void 0 && holder !== pk) throw new MemrelError("UNIQUE_VIOLATION", `${this.describe(table, row)}: unique key (${u.fields.join(", ")}) ${displayKey(row, u.fields)} is already taken by ${this.describe(table, s.rows.get(holder).row)}`);
|
|
441
|
+
this.mapSet(map, value, pk);
|
|
442
|
+
}
|
|
443
|
+
for (const idx of table.indexes) this.bucketAdd(s.indexes[idx.index], encodeFields(row, idx.fields), pk);
|
|
444
|
+
for (const ref of table.refs) {
|
|
445
|
+
const fk = this.foreignKey(ref, row);
|
|
446
|
+
if (typeof fk === "string") this.bucketAdd(s.reverse[ref.index], fk, pk);
|
|
447
|
+
}
|
|
448
|
+
this.mapSet(s.rows, pk, {
|
|
449
|
+
row,
|
|
450
|
+
seq
|
|
451
|
+
});
|
|
452
|
+
op.touched.push([table, pk]);
|
|
453
|
+
}
|
|
454
|
+
removeRow(table, pk, row) {
|
|
455
|
+
const s = table.state;
|
|
456
|
+
for (const u of table.uniques) {
|
|
457
|
+
const value = this.uniqueValue(row, u.fields);
|
|
458
|
+
if (value !== null && s.uniques[u.index].get(value) === pk) this.mapDelete(s.uniques[u.index], value);
|
|
459
|
+
}
|
|
460
|
+
for (const idx of table.indexes) this.bucketDelete(s.indexes[idx.index], encodeFields(row, idx.fields), pk);
|
|
461
|
+
for (const ref of table.refs) {
|
|
462
|
+
const fk = this.foreignKey(ref, row);
|
|
463
|
+
if (typeof fk === "string") this.bucketDelete(s.reverse[ref.index], fk, pk);
|
|
464
|
+
}
|
|
465
|
+
this.mapDelete(s.rows, pk);
|
|
466
|
+
}
|
|
467
|
+
get(table, key) {
|
|
468
|
+
return table.state.rows.get(encodeFields(key, table.key))?.row;
|
|
469
|
+
}
|
|
470
|
+
where(table, conditions) {
|
|
471
|
+
const state = table.state;
|
|
472
|
+
const conds = /* @__PURE__ */ new Map();
|
|
473
|
+
for (const field of Object.keys(conditions)) {
|
|
474
|
+
const raw = conditions[field];
|
|
475
|
+
if (raw === void 0) continue;
|
|
476
|
+
const values = isInCondition(raw) ? raw.in : [raw];
|
|
477
|
+
const encoded = [...new Set(values.map(encodeValue))];
|
|
478
|
+
conds.set(field, {
|
|
479
|
+
field,
|
|
480
|
+
encoded,
|
|
481
|
+
set: new Set(encoded),
|
|
482
|
+
hasNull: values.some(isNullish)
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (conds.size === 0) return inOrder([...state.rows.values()]).map((e) => e.row);
|
|
486
|
+
let best;
|
|
487
|
+
for (const path of this.paths.get(table)) {
|
|
488
|
+
const parts = [];
|
|
489
|
+
for (const f of path.fields) {
|
|
490
|
+
const c = conds.get(f);
|
|
491
|
+
if (!c || path.skipsNulls && c.hasNull) break;
|
|
492
|
+
parts.push(c.encoded);
|
|
493
|
+
}
|
|
494
|
+
if (parts.length !== path.fields.length) continue;
|
|
495
|
+
const keys = combine(parts);
|
|
496
|
+
let size = 0;
|
|
497
|
+
for (const k of keys) {
|
|
498
|
+
const hit = path.lookup(state, k);
|
|
499
|
+
if (hit !== void 0) size += typeof hit === "string" ? 1 : hit.size;
|
|
500
|
+
}
|
|
501
|
+
if (!best || size < best.size) best = {
|
|
502
|
+
path,
|
|
503
|
+
keys,
|
|
504
|
+
size
|
|
505
|
+
};
|
|
506
|
+
if (size === 0) return [];
|
|
507
|
+
}
|
|
508
|
+
let candidates;
|
|
509
|
+
if (best) {
|
|
510
|
+
const found = [];
|
|
511
|
+
for (const k of best.keys) {
|
|
512
|
+
const hit = best.path.lookup(state, k);
|
|
513
|
+
if (hit === void 0) continue;
|
|
514
|
+
if (typeof hit === "string") found.push(state.rows.get(hit));
|
|
515
|
+
else for (const pk of hit) found.push(state.rows.get(pk));
|
|
516
|
+
}
|
|
517
|
+
candidates = found;
|
|
518
|
+
} else candidates = state.rows.values();
|
|
519
|
+
const checks = [...conds.values()];
|
|
520
|
+
const out = [];
|
|
521
|
+
for (const entry of candidates) if (checks.every((c) => c.set.has(encodeValue(entry.row[c.field])))) out.push(entry);
|
|
522
|
+
return inOrder(out).map((e) => e.row);
|
|
523
|
+
}
|
|
524
|
+
rel(table, input, name) {
|
|
525
|
+
const resolve = (fields) => {
|
|
526
|
+
if (fields.every((f) => f in input)) return input;
|
|
527
|
+
return table.state.rows.get(encodeFields(input, table.key))?.row;
|
|
528
|
+
};
|
|
529
|
+
const ref = table.refsByName.get(name);
|
|
530
|
+
if (ref) {
|
|
531
|
+
const row = resolve(ref.fields);
|
|
532
|
+
if (!row) throw new MemrelError("NOT_FOUND", `${table.name} ${displayKey(input, table.key)} not found`);
|
|
533
|
+
const fk = this.foreignKey(ref, row);
|
|
534
|
+
if (fk === null) return null;
|
|
535
|
+
const target = fk === void 0 ? void 0 : this.findTarget(ref, fk);
|
|
536
|
+
if (!target) throw new MemrelError("FOREIGN_KEY_VIOLATION", `${this.describe(table, row)}: ${ref.id} references missing ${ref.to.name} ${displayKey(row, ref.fields)}`);
|
|
537
|
+
return target.row;
|
|
538
|
+
}
|
|
539
|
+
const inverse = table.inverses.get(name);
|
|
540
|
+
if (inverse) {
|
|
541
|
+
const row = resolve(inverse.references);
|
|
542
|
+
const value = row ? this.referencedValue(inverse, row) : null;
|
|
543
|
+
const child = inverse.from;
|
|
544
|
+
const bucket = value === null ? void 0 : child.state.reverse[inverse.index].get(value);
|
|
545
|
+
if (inverse.inverseSingle) {
|
|
546
|
+
if (!bucket) return void 0;
|
|
547
|
+
for (const pk of bucket) return child.state.rows.get(pk).row;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (!bucket) return [];
|
|
551
|
+
const entries = [];
|
|
552
|
+
for (const pk of bucket) entries.push(child.state.rows.get(pk));
|
|
553
|
+
return inOrder(entries).map((e) => e.row);
|
|
554
|
+
}
|
|
555
|
+
const names = [...table.refsByName.keys(), ...table.inverses.keys()];
|
|
556
|
+
throw new MemrelError("INVALID_ARGUMENT", `'${name}' is not a relation of ${table.name}${names.length ? `; expected one of: ${names.join(", ")}` : ""}`);
|
|
557
|
+
}
|
|
558
|
+
insertRow(op, table, input, mode) {
|
|
559
|
+
const row = { ...input };
|
|
560
|
+
const pk = encodeFields(row, table.key);
|
|
561
|
+
if (!table.state.rows.get(pk)) this.addRow(op, table, pk, row, table.state.seq++);
|
|
562
|
+
else if (mode === "replace") this.updateRows(op, table, [[pk, row]], 0);
|
|
563
|
+
else if (mode === "throw") throw new MemrelError("DUPLICATE_KEY", `${this.describe(table, row)} already exists`);
|
|
564
|
+
return pk;
|
|
565
|
+
}
|
|
566
|
+
insert(table, input, mode) {
|
|
567
|
+
return this.write((op) => {
|
|
568
|
+
if (!Array.isArray(input)) {
|
|
569
|
+
const pk = this.insertRow(op, table, input, mode);
|
|
570
|
+
return () => table.state.rows.get(pk).row;
|
|
571
|
+
}
|
|
572
|
+
const pks = input.map((row) => this.insertRow(op, table, row, mode));
|
|
573
|
+
return () => pks.map((pk) => table.state.rows.get(pk).row);
|
|
574
|
+
})();
|
|
575
|
+
}
|
|
576
|
+
load(data, mode) {
|
|
577
|
+
this.write((op) => {
|
|
578
|
+
for (const [name, rows] of Object.entries(data)) {
|
|
579
|
+
if (rows === void 0) continue;
|
|
580
|
+
const table = this.table(name);
|
|
581
|
+
for (const row of rows) this.insertRow(op, table, row, mode);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
table(name) {
|
|
586
|
+
const table = this.tables.get(name);
|
|
587
|
+
if (!table) throw new MemrelError("INVALID_ARGUMENT", `'${name}' is not a collection`);
|
|
588
|
+
return table;
|
|
589
|
+
}
|
|
590
|
+
update(table, keys, patch) {
|
|
591
|
+
return this.write((op) => {
|
|
592
|
+
const many = Array.isArray(keys);
|
|
593
|
+
const list = many ? keys : [keys];
|
|
594
|
+
const changes = /* @__PURE__ */ new Map();
|
|
595
|
+
const pks = list.map((key) => {
|
|
596
|
+
const pk = encodeFields(key, table.key);
|
|
597
|
+
const entry = table.state.rows.get(pk);
|
|
598
|
+
if (!entry) throw new MemrelError("NOT_FOUND", `${table.name} ${displayKey(key, table.key)} not found`);
|
|
599
|
+
if (!changes.has(pk)) {
|
|
600
|
+
const p = typeof patch === "function" ? patch(entry.row) : patch;
|
|
601
|
+
changes.set(pk, {
|
|
602
|
+
...entry.row,
|
|
603
|
+
...p
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return pk;
|
|
607
|
+
});
|
|
608
|
+
const newPks = this.updateRows(op, table, [...changes], 0);
|
|
609
|
+
const moved = new Map([...changes.keys()].map((pk, i) => [pk, newPks[i]]));
|
|
610
|
+
return () => {
|
|
611
|
+
const rows = pks.map((pk) => table.state.rows.get(moved.get(pk)).row);
|
|
612
|
+
return many ? rows : rows[0];
|
|
613
|
+
};
|
|
614
|
+
})();
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Replaces rows and applies the `onUpdate` rules of refs pointing at changed values. All old
|
|
618
|
+
* rows leave the indexes before the new ones enter, so rows in one call may swap keys.
|
|
619
|
+
* Returns the new primary keys, in order.
|
|
620
|
+
*/
|
|
621
|
+
updateRows(op, table, changes, depth) {
|
|
622
|
+
if (depth > MAX_CASCADE_DEPTH) throw new MemrelError("INVALID_ARGUMENT", `Cascade through ${table.name} doesn't terminate`);
|
|
623
|
+
const s = table.state;
|
|
624
|
+
const olds = changes.map(([pk]) => s.rows.get(pk));
|
|
625
|
+
for (let i = 0; i < changes.length; i++) this.removeRow(table, changes[i][0], olds[i].row);
|
|
626
|
+
const newPks = changes.map(([, row], i) => {
|
|
627
|
+
const pk = encodeFields(row, table.key);
|
|
628
|
+
if (s.rows.has(pk)) throw new MemrelError("DUPLICATE_KEY", `Cannot update ${this.describe(table, olds[i].row)}: ${this.describe(table, row)} already exists`);
|
|
629
|
+
this.addRow(op, table, pk, row, olds[i].seq);
|
|
630
|
+
return pk;
|
|
631
|
+
});
|
|
632
|
+
for (const ref of table.incoming) {
|
|
633
|
+
const child = ref.from;
|
|
634
|
+
const childChanges = /* @__PURE__ */ new Map();
|
|
635
|
+
for (let i = 0; i < changes.length; i++) {
|
|
636
|
+
const row = changes[i][1];
|
|
637
|
+
const oldRow = olds[i].row;
|
|
638
|
+
const before = this.referencedValue(ref, oldRow);
|
|
639
|
+
if (before === null || before === this.referencedValue(ref, row)) continue;
|
|
640
|
+
const bucket = child.state.reverse[ref.index].get(before);
|
|
641
|
+
if (!bucket || bucket.size === 0) continue;
|
|
642
|
+
if (ref.onUpdate === "restrict") {
|
|
643
|
+
op.vacated.push({
|
|
644
|
+
ref,
|
|
645
|
+
value: before,
|
|
646
|
+
verb: "update",
|
|
647
|
+
row: oldRow
|
|
648
|
+
});
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
for (const childPk of bucket) {
|
|
652
|
+
const next = { ...childChanges.get(childPk) ?? child.state.rows.get(childPk).row };
|
|
653
|
+
if (ref.onUpdate === "cascade") for (let j = 0; j < ref.fields.length; j++) next[ref.fields[j]] = row[ref.references[j]];
|
|
654
|
+
else for (const f of ref.nullFields) next[f] = null;
|
|
655
|
+
childChanges.set(childPk, next);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (childChanges.size > 0) this.updateRows(op, child, [...childChanges], depth + 1);
|
|
659
|
+
}
|
|
660
|
+
return newPks;
|
|
661
|
+
}
|
|
662
|
+
delete(roots) {
|
|
663
|
+
return this.write((op) => {
|
|
664
|
+
const plan = /* @__PURE__ */ new Map();
|
|
665
|
+
const queue = [];
|
|
666
|
+
const planned = (table) => {
|
|
667
|
+
let rows = plan.get(table);
|
|
668
|
+
if (!rows) {
|
|
669
|
+
rows = /* @__PURE__ */ new Map();
|
|
670
|
+
plan.set(table, rows);
|
|
671
|
+
}
|
|
672
|
+
return rows;
|
|
673
|
+
};
|
|
674
|
+
for (const [table, key] of roots) {
|
|
675
|
+
const pk = encodeFields(key, table.key);
|
|
676
|
+
const entry = table.state.rows.get(pk);
|
|
677
|
+
if (!entry) throw new MemrelError("NOT_FOUND", `${table.name} ${displayKey(key, table.key)} not found`);
|
|
678
|
+
const rows = planned(table);
|
|
679
|
+
if (rows.has(pk)) continue;
|
|
680
|
+
rows.set(pk, entry.row);
|
|
681
|
+
queue.push([table, entry.row]);
|
|
682
|
+
}
|
|
683
|
+
const nulls = /* @__PURE__ */ new Map();
|
|
684
|
+
for (let i = 0; i < queue.length; i++) {
|
|
685
|
+
const [table, row] = queue[i];
|
|
686
|
+
for (const ref of table.incoming) {
|
|
687
|
+
const value = this.referencedValue(ref, row);
|
|
688
|
+
if (value === null) continue;
|
|
689
|
+
const child = ref.from;
|
|
690
|
+
const bucket = child.state.reverse[ref.index].get(value);
|
|
691
|
+
if (!bucket || bucket.size === 0) continue;
|
|
692
|
+
if (ref.onDelete === "restrict") op.vacated.push({
|
|
693
|
+
ref,
|
|
694
|
+
value,
|
|
695
|
+
verb: "delete",
|
|
696
|
+
row
|
|
697
|
+
});
|
|
698
|
+
else if (ref.onDelete === "cascade") {
|
|
699
|
+
const rows = planned(child);
|
|
700
|
+
for (const childPk of bucket) {
|
|
701
|
+
if (rows.has(childPk)) continue;
|
|
702
|
+
const childRow = child.state.rows.get(childPk).row;
|
|
703
|
+
rows.set(childPk, childRow);
|
|
704
|
+
queue.push([child, childRow]);
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
let byPk = nulls.get(child);
|
|
708
|
+
if (!byPk) {
|
|
709
|
+
byPk = /* @__PURE__ */ new Map();
|
|
710
|
+
nulls.set(child, byPk);
|
|
711
|
+
}
|
|
712
|
+
for (const childPk of bucket) {
|
|
713
|
+
const refs = byPk.get(childPk);
|
|
714
|
+
if (refs) refs.push(ref);
|
|
715
|
+
else byPk.set(childPk, [ref]);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const outcome = {
|
|
721
|
+
deleted: {},
|
|
722
|
+
nulled: {}
|
|
723
|
+
};
|
|
724
|
+
for (const [table, rows] of plan) for (const [pk, row] of rows) {
|
|
725
|
+
this.removeRow(table, pk, row);
|
|
726
|
+
push(outcome.deleted, table.name, row);
|
|
727
|
+
}
|
|
728
|
+
const nulled = [];
|
|
729
|
+
for (const [table, byPk] of nulls) {
|
|
730
|
+
const changes = [];
|
|
731
|
+
for (const [pk, refs] of byPk) {
|
|
732
|
+
const entry = table.state.rows.get(pk);
|
|
733
|
+
if (!entry) continue;
|
|
734
|
+
const next = { ...entry.row };
|
|
735
|
+
for (const ref of refs) for (const f of ref.nullFields) next[f] = null;
|
|
736
|
+
changes.push([pk, next]);
|
|
737
|
+
}
|
|
738
|
+
if (changes.length > 0) nulled.push([table, this.updateRows(op, table, changes, 0)]);
|
|
739
|
+
}
|
|
740
|
+
return () => {
|
|
741
|
+
for (const [table, pks] of nulled) for (const pk of pks) {
|
|
742
|
+
const entry = table.state.rows.get(pk);
|
|
743
|
+
if (entry) push(outcome.nulled, table.name, entry.row);
|
|
744
|
+
}
|
|
745
|
+
return outcome;
|
|
746
|
+
};
|
|
747
|
+
})();
|
|
748
|
+
}
|
|
749
|
+
snapshot() {
|
|
750
|
+
const states = /* @__PURE__ */ new Map();
|
|
751
|
+
for (const table of this.tables.values()) states.set(table, cloneState(table.state));
|
|
752
|
+
return {
|
|
753
|
+
store: this,
|
|
754
|
+
states
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
restore(snapshot) {
|
|
758
|
+
if (snapshot?.store !== this) throw new MemrelError("INVALID_ARGUMENT", "Snapshot was not taken from this database");
|
|
759
|
+
this.savepoint(() => {
|
|
760
|
+
for (const table of this.tables.values()) {
|
|
761
|
+
const prev = table.state;
|
|
762
|
+
table.state = cloneState(snapshot.states.get(table));
|
|
763
|
+
this.log(() => {
|
|
764
|
+
table.state = prev;
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
// src/db.ts
|
|
771
|
+
function conflictMode(options) {
|
|
772
|
+
const mode = options?.onConflict ?? "throw";
|
|
773
|
+
if (mode !== "throw" && mode !== "ignore" && mode !== "replace") throw new MemrelError("INVALID_ARGUMENT", `Unknown onConflict mode '${String(mode)}'`);
|
|
774
|
+
return mode;
|
|
775
|
+
}
|
|
776
|
+
var CollectionApi = class {
|
|
777
|
+
#store;
|
|
778
|
+
#table;
|
|
779
|
+
constructor(store, table) {
|
|
780
|
+
this.#store = store;
|
|
781
|
+
this.#table = table;
|
|
782
|
+
}
|
|
783
|
+
get(key) {
|
|
784
|
+
return this.#store.get(this.#table, key);
|
|
785
|
+
}
|
|
786
|
+
where(conditions) {
|
|
787
|
+
return this.#store.where(this.#table, conditions);
|
|
788
|
+
}
|
|
789
|
+
rel(row, name) {
|
|
790
|
+
return this.#store.rel(this.#table, row, name);
|
|
791
|
+
}
|
|
792
|
+
insert(rows, options) {
|
|
793
|
+
return this.#store.insert(this.#table, rows, conflictMode(options));
|
|
794
|
+
}
|
|
795
|
+
update(keys, patch) {
|
|
796
|
+
return this.#store.update(this.#table, keys, patch);
|
|
797
|
+
}
|
|
798
|
+
delete(keys) {
|
|
799
|
+
const list = Array.isArray(keys) ? keys : [keys];
|
|
800
|
+
return this.#store.delete(list.map((key) => [this.#table, key]));
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
/**
|
|
804
|
+
* Creates a database. The double call is deliberate: the first call fixes the row types, the
|
|
805
|
+
* second infers the exact config, which TypeScript can't do in one call.
|
|
806
|
+
*
|
|
807
|
+
* @example
|
|
808
|
+
* ```ts
|
|
809
|
+
* const db = createDb<{ user: User; post: Post }>()({
|
|
810
|
+
* user: { key: ['id'], unique: [['email']] },
|
|
811
|
+
* post: {
|
|
812
|
+
* key: ['id'],
|
|
813
|
+
* refs: {
|
|
814
|
+
* author: { fields: ['authorId'], to: 'user', onDelete: 'cascade', inverse: 'posts' },
|
|
815
|
+
* },
|
|
816
|
+
* },
|
|
817
|
+
* });
|
|
818
|
+
* ```
|
|
819
|
+
*/
|
|
820
|
+
function createDb() {
|
|
821
|
+
return defineDb;
|
|
822
|
+
}
|
|
823
|
+
function defineDb(config) {
|
|
824
|
+
const store = new Store(config);
|
|
825
|
+
const db = {
|
|
826
|
+
load(data, options) {
|
|
827
|
+
store.load(data, conflictMode(options));
|
|
828
|
+
},
|
|
829
|
+
remove(keys) {
|
|
830
|
+
const roots = [];
|
|
831
|
+
for (const [name, list] of Object.entries(keys)) {
|
|
832
|
+
const table = store.table(name);
|
|
833
|
+
for (const key of list ?? []) roots.push([table, key]);
|
|
834
|
+
}
|
|
835
|
+
return store.delete(roots);
|
|
836
|
+
},
|
|
837
|
+
transaction(fn) {
|
|
838
|
+
return store.savepoint(fn);
|
|
839
|
+
},
|
|
840
|
+
snapshot() {
|
|
841
|
+
return store.snapshot();
|
|
842
|
+
},
|
|
843
|
+
restore(snapshot) {
|
|
844
|
+
store.restore(snapshot);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
for (const table of store.tables.values()) db[table.name] = new CollectionApi(store, table);
|
|
848
|
+
return db;
|
|
849
|
+
}
|
|
850
|
+
export { MemrelError, createDb };
|
|
851
|
+
|
|
852
|
+
//# sourceMappingURL=index.js.map
|