@monlite/core 0.5.0 → 0.7.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/README.md +96 -14
- package/dist/index.cjs +457 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -8
- package/dist/index.d.ts +79 -8
- package/dist/index.js +453 -131
- package/dist/index.js.map +1 -1
- package/package.json +8 -1
package/dist/index.cjs
CHANGED
|
@@ -22,17 +22,64 @@ function isObjectId(value) {
|
|
|
22
22
|
|
|
23
23
|
// src/errors.ts
|
|
24
24
|
var MonliteError = class extends Error {
|
|
25
|
-
constructor(message) {
|
|
25
|
+
constructor(message, options) {
|
|
26
26
|
super(message);
|
|
27
27
|
this.name = "MonliteError";
|
|
28
|
+
if (options?.cause !== void 0) this.cause = options.cause;
|
|
28
29
|
}
|
|
29
30
|
};
|
|
30
31
|
var MonliteQueryError = class extends MonliteError {
|
|
31
|
-
constructor(message) {
|
|
32
|
-
super(message);
|
|
32
|
+
constructor(message, options) {
|
|
33
|
+
super(message, options);
|
|
33
34
|
this.name = "MonliteQueryError";
|
|
34
35
|
}
|
|
35
36
|
};
|
|
37
|
+
var MonliteConstraintError = class extends MonliteError {
|
|
38
|
+
collection;
|
|
39
|
+
constructor(message, options) {
|
|
40
|
+
super(message, options);
|
|
41
|
+
this.name = "MonliteConstraintError";
|
|
42
|
+
this.collection = options?.collection;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var MonliteUniqueConstraintError = class extends MonliteConstraintError {
|
|
46
|
+
constructor(message, options) {
|
|
47
|
+
super(message, options);
|
|
48
|
+
this.name = "MonliteUniqueConstraintError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var MonliteNotNullError = class extends MonliteConstraintError {
|
|
52
|
+
constructor(message, options) {
|
|
53
|
+
super(message, options);
|
|
54
|
+
this.name = "MonliteNotNullError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var MonliteForeignKeyError = class extends MonliteConstraintError {
|
|
58
|
+
constructor(message, options) {
|
|
59
|
+
super(message, options);
|
|
60
|
+
this.name = "MonliteForeignKeyError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
function normalizeDriverError(err, collection) {
|
|
64
|
+
if (err instanceof MonliteError) return err;
|
|
65
|
+
const code = err?.code ? String(err.code) : "";
|
|
66
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
67
|
+
const blob = `${code} ${message}`;
|
|
68
|
+
const opts = { cause: err, collection };
|
|
69
|
+
if (/UNIQUE|PRIMARY KEY|constraint failed: .*\.(?:_id)\b/i.test(blob)) {
|
|
70
|
+
return new MonliteUniqueConstraintError(message, opts);
|
|
71
|
+
}
|
|
72
|
+
if (/NOT ?NULL/i.test(blob)) {
|
|
73
|
+
return new MonliteNotNullError(message, opts);
|
|
74
|
+
}
|
|
75
|
+
if (/FOREIGN ?KEY/i.test(blob)) {
|
|
76
|
+
return new MonliteForeignKeyError(message, opts);
|
|
77
|
+
}
|
|
78
|
+
if (/CONSTRAINT/i.test(blob)) {
|
|
79
|
+
return new MonliteConstraintError(message, opts);
|
|
80
|
+
}
|
|
81
|
+
return new MonliteError(message, { cause: err });
|
|
82
|
+
}
|
|
36
83
|
|
|
37
84
|
// src/query/sql.ts
|
|
38
85
|
var RESERVED_FIELDS = /* @__PURE__ */ new Set(["_id", "created_at", "updated_at"]);
|
|
@@ -59,9 +106,12 @@ function pathLiteral(field) {
|
|
|
59
106
|
return "'" + jsonPath(field).replace(/'/g, "''") + "'";
|
|
60
107
|
}
|
|
61
108
|
function fieldExpr(field, columns) {
|
|
62
|
-
if (isColumn(field, columns)) return
|
|
109
|
+
if (isColumn(field, columns)) return quoteIdent(field);
|
|
63
110
|
return `json_extract(data, ${pathLiteral(field)})`;
|
|
64
111
|
}
|
|
112
|
+
function quoteIdent(name) {
|
|
113
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
114
|
+
}
|
|
65
115
|
function bindable(value) {
|
|
66
116
|
if (value === void 0 || value === null) return null;
|
|
67
117
|
if (typeof value === "boolean") return value ? 1 : 0;
|
|
@@ -112,10 +162,11 @@ function translateField(field, condition, ctx) {
|
|
|
112
162
|
return eqExpr(expr, condition, ctx);
|
|
113
163
|
}
|
|
114
164
|
const filter = condition;
|
|
165
|
+
const ci = filter.mode === "insensitive";
|
|
115
166
|
const clauses = [];
|
|
116
167
|
for (const op of Object.keys(filter)) {
|
|
117
168
|
const v = filter[op];
|
|
118
|
-
if (v === void 0) continue;
|
|
169
|
+
if (v === void 0 || op === "mode") continue;
|
|
119
170
|
switch (op) {
|
|
120
171
|
case "equals":
|
|
121
172
|
clauses.push(eqExpr(expr, v, ctx));
|
|
@@ -142,16 +193,20 @@ function translateField(field, condition, ctx) {
|
|
|
142
193
|
clauses.push(inExpr(expr, v, ctx, true));
|
|
143
194
|
break;
|
|
144
195
|
case "contains":
|
|
145
|
-
clauses.push(containsExpr(field, expr, v, ctx));
|
|
196
|
+
clauses.push(containsExpr(field, expr, v, ctx, ci));
|
|
146
197
|
break;
|
|
147
198
|
case "startsWith":
|
|
148
199
|
ctx.params.push(bindable(v));
|
|
149
|
-
clauses.push(
|
|
200
|
+
clauses.push(
|
|
201
|
+
ci ? `instr(lower(${expr}), lower(?)) = 1` : `instr(${expr}, ?) = 1`
|
|
202
|
+
);
|
|
150
203
|
break;
|
|
151
204
|
case "endsWith":
|
|
152
205
|
ctx.params.push(bindable(v));
|
|
153
206
|
ctx.params.push(bindable(v));
|
|
154
|
-
clauses.push(
|
|
207
|
+
clauses.push(
|
|
208
|
+
ci ? `substr(lower(${expr}), -length(?)) = lower(?)` : `substr(${expr}, -length(?)) = ?`
|
|
209
|
+
);
|
|
155
210
|
break;
|
|
156
211
|
case "has":
|
|
157
212
|
clauses.push(hasExpr(field, expr, v, ctx));
|
|
@@ -184,9 +239,7 @@ function cmp(expr, op, v, ctx) {
|
|
|
184
239
|
}
|
|
185
240
|
function inExpr(expr, arr, ctx, negate) {
|
|
186
241
|
if (!Array.isArray(arr)) {
|
|
187
|
-
throw new MonliteQueryError(
|
|
188
|
-
`${negate ? "notIn" : "in"} expects an array`
|
|
189
|
-
);
|
|
242
|
+
throw new MonliteQueryError(`${negate ? "notIn" : "in"} expects an array`);
|
|
190
243
|
}
|
|
191
244
|
if (arr.length === 0) return negate ? "1" : "0";
|
|
192
245
|
const placeholders = arr.map((v) => {
|
|
@@ -195,15 +248,17 @@ function inExpr(expr, arr, ctx, negate) {
|
|
|
195
248
|
}).join(", ");
|
|
196
249
|
return negate ? `(${expr} IS NULL OR ${expr} NOT IN (${placeholders}))` : `${expr} IN (${placeholders})`;
|
|
197
250
|
}
|
|
198
|
-
function containsExpr(field, expr, v, ctx) {
|
|
251
|
+
function containsExpr(field, expr, v, ctx, ci) {
|
|
252
|
+
const sub = ci ? `instr(lower(${expr}), lower(?)) > 0` : `instr(${expr}, ?) > 0`;
|
|
199
253
|
if (isColumn(field, ctx.columns)) {
|
|
200
254
|
ctx.params.push(bindable(v));
|
|
201
|
-
return
|
|
255
|
+
return sub;
|
|
202
256
|
}
|
|
203
257
|
const path = pathLiteral(field);
|
|
258
|
+
const member = ci ? `lower(value) = lower(?)` : `value = ?`;
|
|
204
259
|
ctx.params.push(bindable(v));
|
|
205
260
|
ctx.params.push(bindable(v));
|
|
206
|
-
return `(CASE WHEN json_type(data, ${path}) = 'array' THEN EXISTS (SELECT 1 FROM json_each(data, ${path}) WHERE
|
|
261
|
+
return `(CASE WHEN json_type(data, ${path}) = 'array' THEN EXISTS (SELECT 1 FROM json_each(data, ${path}) WHERE ${member}) ELSE ${sub} END)`;
|
|
207
262
|
}
|
|
208
263
|
function hasExpr(field, expr, v, ctx) {
|
|
209
264
|
ctx.params.push(bindable(v));
|
|
@@ -236,16 +291,26 @@ function buildOrderBy(orderBy, onPath, columns) {
|
|
|
236
291
|
}
|
|
237
292
|
|
|
238
293
|
// src/query/path.ts
|
|
294
|
+
var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
295
|
+
function safeSegments(path) {
|
|
296
|
+
const segs = path.split(".");
|
|
297
|
+
for (const seg of segs) {
|
|
298
|
+
if (FORBIDDEN.has(seg)) {
|
|
299
|
+
throw new MonliteQueryError(`Illegal path segment "${seg}" in "${path}"`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return segs;
|
|
303
|
+
}
|
|
239
304
|
function getPath(obj, path) {
|
|
240
305
|
let cur = obj;
|
|
241
|
-
for (const seg of path
|
|
306
|
+
for (const seg of safeSegments(path)) {
|
|
242
307
|
if (cur == null) return void 0;
|
|
243
308
|
cur = cur[seg];
|
|
244
309
|
}
|
|
245
310
|
return cur;
|
|
246
311
|
}
|
|
247
312
|
function setPath(obj, path, value) {
|
|
248
|
-
const segs = path
|
|
313
|
+
const segs = safeSegments(path);
|
|
249
314
|
let cur = obj;
|
|
250
315
|
for (let i = 0; i < segs.length - 1; i++) {
|
|
251
316
|
const seg = segs[i];
|
|
@@ -255,7 +320,7 @@ function setPath(obj, path, value) {
|
|
|
255
320
|
cur[segs[segs.length - 1]] = value;
|
|
256
321
|
}
|
|
257
322
|
function unsetPath(obj, path) {
|
|
258
|
-
const segs = path
|
|
323
|
+
const segs = safeSegments(path);
|
|
259
324
|
let cur = obj;
|
|
260
325
|
for (let i = 0; i < segs.length - 1; i++) {
|
|
261
326
|
const seg = segs[i];
|
|
@@ -304,12 +369,18 @@ function applyUpdate(doc, data) {
|
|
|
304
369
|
}
|
|
305
370
|
const ops = data;
|
|
306
371
|
if (ops.$set) {
|
|
307
|
-
for (const [path, value] of Object.entries(ops.$set))
|
|
372
|
+
for (const [path, value] of Object.entries(ops.$set))
|
|
373
|
+
setPath(next, path, value);
|
|
308
374
|
}
|
|
309
375
|
if (ops.$inc) {
|
|
310
376
|
for (const [path, by] of Object.entries(ops.$inc)) {
|
|
377
|
+
if (typeof by !== "number" || !Number.isFinite(by)) {
|
|
378
|
+
throw new MonliteQueryError(
|
|
379
|
+
`$inc on "${path}" requires a finite number, got ${JSON.stringify(by)}`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
311
382
|
const cur = getPath(next, path);
|
|
312
|
-
setPath(next, path, (typeof cur === "number" ? cur : 0) +
|
|
383
|
+
setPath(next, path, (typeof cur === "number" ? cur : 0) + by);
|
|
313
384
|
}
|
|
314
385
|
}
|
|
315
386
|
if (ops.$push) {
|
|
@@ -420,7 +491,11 @@ function buildHaving(having, params, columns) {
|
|
|
420
491
|
if (!selection) continue;
|
|
421
492
|
for (const field of Object.keys(selection)) {
|
|
422
493
|
parts.push(
|
|
423
|
-
...comparisonSql(
|
|
494
|
+
...comparisonSql(
|
|
495
|
+
`${fn}(${fieldExpr(field, columns)})`,
|
|
496
|
+
selection[field],
|
|
497
|
+
params
|
|
498
|
+
)
|
|
424
499
|
);
|
|
425
500
|
}
|
|
426
501
|
}
|
|
@@ -428,7 +503,7 @@ function buildHaving(having, params, columns) {
|
|
|
428
503
|
}
|
|
429
504
|
function groupBy(ctx, args) {
|
|
430
505
|
if (!Array.isArray(args.by) || args.by.length === 0) {
|
|
431
|
-
throw new
|
|
506
|
+
throw new MonliteQueryError("groupBy requires a non-empty `by` array");
|
|
432
507
|
}
|
|
433
508
|
const params = [];
|
|
434
509
|
const where = buildWhere(args.where, {
|
|
@@ -437,15 +512,22 @@ function groupBy(ctx, args) {
|
|
|
437
512
|
columns: ctx.columns
|
|
438
513
|
});
|
|
439
514
|
const groupExprs = [];
|
|
515
|
+
const groupCols = [];
|
|
440
516
|
const selects = [];
|
|
441
|
-
|
|
517
|
+
args.by.forEach((field, gi) => {
|
|
442
518
|
if (!isColumn(field, ctx.columns)) ctx.onPath(field);
|
|
443
519
|
const expr = fieldExpr(field, ctx.columns);
|
|
520
|
+
const alias = `grp_${gi}`;
|
|
444
521
|
groupExprs.push(expr);
|
|
445
|
-
selects.push(`${expr} AS
|
|
446
|
-
|
|
522
|
+
selects.push(`${expr} AS ${alias}`);
|
|
523
|
+
groupCols.push({ alias, field });
|
|
524
|
+
});
|
|
447
525
|
selects.push(`COUNT(*) AS agg_count`);
|
|
448
|
-
const { selects: accSelects, cols } = buildAccumulators(
|
|
526
|
+
const { selects: accSelects, cols } = buildAccumulators(
|
|
527
|
+
args,
|
|
528
|
+
ctx.onPath,
|
|
529
|
+
ctx.columns
|
|
530
|
+
);
|
|
449
531
|
selects.push(...accSelects);
|
|
450
532
|
let sql = `SELECT ${selects.join(", ")} FROM "${ctx.table}" WHERE ${where} GROUP BY ${groupExprs.join(", ")}`;
|
|
451
533
|
if (args.having) {
|
|
@@ -472,7 +554,7 @@ function groupBy(ctx, args) {
|
|
|
472
554
|
const rows = ctx.db.prepare(sql).all(...params);
|
|
473
555
|
return rows.map((row) => {
|
|
474
556
|
const out = {};
|
|
475
|
-
for (const field of
|
|
557
|
+
for (const { alias, field } of groupCols) out[field] = row[alias];
|
|
476
558
|
if (args._count) out._count = row.agg_count;
|
|
477
559
|
for (const col of cols) {
|
|
478
560
|
(out[col.kind] ??= {})[col.field] = row[col.alias] ?? null;
|
|
@@ -511,6 +593,13 @@ var Collection = class {
|
|
|
511
593
|
);
|
|
512
594
|
}
|
|
513
595
|
const normalized = typeof def === "string" ? { type: def } : def;
|
|
596
|
+
if (normalized.references && !/^[A-Za-z_][A-Za-z0-9_]*(\([A-Za-z_][A-Za-z0-9_]*\))?$/.test(
|
|
597
|
+
normalized.references
|
|
598
|
+
)) {
|
|
599
|
+
throw new MonliteError(
|
|
600
|
+
`Invalid references "${normalized.references}" on column "${field}"`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
514
603
|
this.columnDefs[field] = normalized;
|
|
515
604
|
this.columnOrder.push(field);
|
|
516
605
|
this.columns.add(field);
|
|
@@ -532,6 +621,14 @@ var Collection = class {
|
|
|
532
621
|
get db() {
|
|
533
622
|
return this.mon.driver;
|
|
534
623
|
}
|
|
624
|
+
/** Run a DB operation, normalizing driver errors into typed MonliteErrors. */
|
|
625
|
+
guard(fn) {
|
|
626
|
+
try {
|
|
627
|
+
return fn();
|
|
628
|
+
} catch (err) {
|
|
629
|
+
throw normalizeDriverError(err, this.name);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
535
632
|
/** Native column names declared for this collection (structured mode). */
|
|
536
633
|
get columnNames() {
|
|
537
634
|
return [...this.columnOrder];
|
|
@@ -559,7 +656,8 @@ var Collection = class {
|
|
|
559
656
|
let line = `"${field}" ${sqliteType(def.type)}`;
|
|
560
657
|
if (def.notNull) line += " NOT NULL";
|
|
561
658
|
if (def.unique) line += " UNIQUE";
|
|
562
|
-
if (def.default !== void 0)
|
|
659
|
+
if (def.default !== void 0)
|
|
660
|
+
line += ` DEFAULT ${formatDefault(def.default)}`;
|
|
563
661
|
if (def.references) line += ` REFERENCES ${def.references}`;
|
|
564
662
|
lines.push(line);
|
|
565
663
|
}
|
|
@@ -584,7 +682,11 @@ var Collection = class {
|
|
|
584
682
|
if (this.mode === "structured") {
|
|
585
683
|
for (const field of this.columnOrder) {
|
|
586
684
|
const value = row[field];
|
|
587
|
-
if (value ===
|
|
685
|
+
if (value === void 0) continue;
|
|
686
|
+
if (value === null) {
|
|
687
|
+
doc[field] = null;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
588
690
|
doc[field] = this.jsonColumns.has(field) ? JSON.parse(value) : value;
|
|
589
691
|
}
|
|
590
692
|
}
|
|
@@ -597,6 +699,11 @@ var Collection = class {
|
|
|
597
699
|
if (this.jsonColumns.has(field)) {
|
|
598
700
|
return value === void 0 ? null : JSON.stringify(value);
|
|
599
701
|
}
|
|
702
|
+
if (value !== null && typeof value === "object" && !(value instanceof Date) && !Buffer.isBuffer(value)) {
|
|
703
|
+
throw new MonliteQueryError(
|
|
704
|
+
`Column "${field}" cannot store an object/array. Declare it as { type: "JSON" } to store structured values.`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
600
707
|
return bindable(value);
|
|
601
708
|
}
|
|
602
709
|
insertColumns() {
|
|
@@ -614,7 +721,12 @@ var Collection = class {
|
|
|
614
721
|
const now = Date.now();
|
|
615
722
|
const id = input._id != null ? String(input._id) : objectId();
|
|
616
723
|
const doc = stripSystem(input);
|
|
617
|
-
const returned = {
|
|
724
|
+
const returned = {
|
|
725
|
+
...doc,
|
|
726
|
+
_id: id,
|
|
727
|
+
created_at: now,
|
|
728
|
+
updated_at: now
|
|
729
|
+
};
|
|
618
730
|
if (this.mode === "document") {
|
|
619
731
|
return {
|
|
620
732
|
_id: id,
|
|
@@ -666,9 +778,65 @@ var Collection = class {
|
|
|
666
778
|
];
|
|
667
779
|
return { setSql: setParts.join(", "), values };
|
|
668
780
|
}
|
|
669
|
-
/** Sync store
|
|
781
|
+
/** Sync store for recording local changes (both document and structured). */
|
|
670
782
|
get recorder() {
|
|
671
|
-
return this.
|
|
783
|
+
return this.mon.$sync;
|
|
784
|
+
}
|
|
785
|
+
/** @internal Read a full document by id (mode-aware), synchronously. */
|
|
786
|
+
getRaw(id) {
|
|
787
|
+
this.ensureTable();
|
|
788
|
+
const row = this.db.prepare(`SELECT * FROM "${this.name}" WHERE _id = ?`).get(id);
|
|
789
|
+
return row ? this.rowToDoc(row) : null;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* @internal Apply a remote change to storage WITHOUT recording it to the
|
|
793
|
+
* change feed (the sync store records the `remote` feed row itself). Used by
|
|
794
|
+
* `@monlite/sync` so structured collections sync correctly through the same
|
|
795
|
+
* column/overflow split as local writes.
|
|
796
|
+
*/
|
|
797
|
+
applyRemoteWrite(op, id, doc, ts) {
|
|
798
|
+
this.ensureTable();
|
|
799
|
+
if (op === "delete") {
|
|
800
|
+
this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`).run(id);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const clean = stripSystem(doc ?? {});
|
|
804
|
+
const createdAt = typeof doc?.created_at === "number" ? doc.created_at : ts;
|
|
805
|
+
if (this.mode === "document") {
|
|
806
|
+
this.db.prepare(
|
|
807
|
+
`INSERT INTO "${this.name}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
808
|
+
ON CONFLICT(_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
|
|
809
|
+
).run(id, JSON.stringify(clean), createdAt, ts);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const overflow = {};
|
|
813
|
+
const colValues = {};
|
|
814
|
+
for (const [k, v] of Object.entries(clean)) {
|
|
815
|
+
if (this.columns.has(k)) colValues[k] = v;
|
|
816
|
+
else overflow[k] = v;
|
|
817
|
+
}
|
|
818
|
+
const cols = [
|
|
819
|
+
"_id",
|
|
820
|
+
"created_at",
|
|
821
|
+
"updated_at",
|
|
822
|
+
"data",
|
|
823
|
+
...this.columnOrder
|
|
824
|
+
];
|
|
825
|
+
const values = [
|
|
826
|
+
id,
|
|
827
|
+
createdAt,
|
|
828
|
+
ts,
|
|
829
|
+
JSON.stringify(overflow),
|
|
830
|
+
...this.columnOrder.map(
|
|
831
|
+
(c) => c in colValues ? this.encodeColumn(c, colValues[c]) : null
|
|
832
|
+
)
|
|
833
|
+
];
|
|
834
|
+
const colList = cols.map((c) => `"${c}"`).join(", ");
|
|
835
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
836
|
+
const updateSet = cols.filter((c) => c !== "_id" && c !== "created_at").map((c) => `"${c}" = excluded."${c}"`).join(", ");
|
|
837
|
+
this.db.prepare(
|
|
838
|
+
`INSERT INTO "${this.name}" (${colList}) VALUES (${placeholders}) ON CONFLICT(_id) DO UPDATE SET ${updateSet}`
|
|
839
|
+
).run(...values);
|
|
672
840
|
}
|
|
673
841
|
/* ----------------------------- create ----------------------------- */
|
|
674
842
|
async create(args) {
|
|
@@ -679,21 +847,22 @@ var Collection = class {
|
|
|
679
847
|
this.db.prepare(this.insertSql()).run(...row.values);
|
|
680
848
|
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
681
849
|
};
|
|
682
|
-
|
|
683
|
-
else write();
|
|
850
|
+
this.guard(() => recorder ? this.db.transaction(write) : write());
|
|
684
851
|
return row.returned;
|
|
685
852
|
}
|
|
686
853
|
async createMany(args) {
|
|
687
854
|
this.ensureTable();
|
|
688
855
|
const stmt = this.db.prepare(this.insertSql());
|
|
689
856
|
const recorder = this.recorder;
|
|
690
|
-
this.
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
857
|
+
this.guard(
|
|
858
|
+
() => this.db.transaction(() => {
|
|
859
|
+
for (const item of args.data) {
|
|
860
|
+
const row = this.buildInsert(item);
|
|
861
|
+
stmt.run(...row.values);
|
|
862
|
+
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
863
|
+
}
|
|
864
|
+
})
|
|
865
|
+
);
|
|
697
866
|
return { count: args.data.length };
|
|
698
867
|
}
|
|
699
868
|
/* ------------------------------ read ------------------------------ */
|
|
@@ -723,6 +892,28 @@ var Collection = class {
|
|
|
723
892
|
const rows = await this.findMany({ ...args, take: 1 });
|
|
724
893
|
return rows[0] ?? null;
|
|
725
894
|
}
|
|
895
|
+
/** Alias of {@link findFirst} for Prisma familiarity. */
|
|
896
|
+
async findUnique(args = {}) {
|
|
897
|
+
return this.findFirst(args);
|
|
898
|
+
}
|
|
899
|
+
/** Like {@link findFirst} but throws if no document matches. */
|
|
900
|
+
async findFirstOrThrow(args = {}) {
|
|
901
|
+
const doc = await this.findFirst(args);
|
|
902
|
+
if (!doc) throw new MonliteError(`No document found in "${this.name}"`);
|
|
903
|
+
return doc;
|
|
904
|
+
}
|
|
905
|
+
/** True if at least one document matches. */
|
|
906
|
+
async exists(where) {
|
|
907
|
+
this.ensureTable();
|
|
908
|
+
const params = [];
|
|
909
|
+
const clause = buildWhere(where, {
|
|
910
|
+
params,
|
|
911
|
+
onPath: this.trackPath,
|
|
912
|
+
columns: this.columns
|
|
913
|
+
});
|
|
914
|
+
const row = this.db.prepare(`SELECT 1 FROM "${this.name}" WHERE ${clause} LIMIT 1`).get(...params);
|
|
915
|
+
return row != null;
|
|
916
|
+
}
|
|
726
917
|
async findById(id) {
|
|
727
918
|
this.ensureTable();
|
|
728
919
|
const row = this.db.prepare(`SELECT * FROM "${this.name}" WHERE _id = ?`).get(id);
|
|
@@ -758,8 +949,10 @@ var Collection = class {
|
|
|
758
949
|
this.trackPath(field);
|
|
759
950
|
sql = `SELECT DISTINCT je.value AS v FROM "${this.name}" CROSS JOIN json_each("${this.name}".data, ${pathLiteral(field)}) je WHERE ${clause} ORDER BY v`;
|
|
760
951
|
}
|
|
761
|
-
|
|
762
|
-
|
|
952
|
+
return this.guard(() => {
|
|
953
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
954
|
+
return rows.map((r) => r.v);
|
|
955
|
+
});
|
|
763
956
|
}
|
|
764
957
|
/* ----------------------------- update ----------------------------- */
|
|
765
958
|
runUpdate(where, data, single) {
|
|
@@ -776,23 +969,25 @@ var Collection = class {
|
|
|
776
969
|
if (!rows.length) return [];
|
|
777
970
|
const now = Date.now();
|
|
778
971
|
const recorder = this.recorder;
|
|
779
|
-
return this.
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
972
|
+
return this.guard(
|
|
973
|
+
() => this.db.transaction(() => {
|
|
974
|
+
const out = [];
|
|
975
|
+
for (const row of rows) {
|
|
976
|
+
const current = stripSystem(this.rowToDoc(row));
|
|
977
|
+
const updated = stripSystem(applyUpdate(current, data));
|
|
978
|
+
const { setSql, values } = this.buildUpdateSet(updated, now);
|
|
979
|
+
this.db.prepare(`UPDATE "${this.name}" SET ${setSql} WHERE _id = ?`).run(...values, row._id);
|
|
980
|
+
recorder?.recordLocal(this.name, row._id, "upsert", now);
|
|
981
|
+
out.push({
|
|
982
|
+
...updated,
|
|
983
|
+
_id: row._id,
|
|
984
|
+
created_at: row.created_at,
|
|
985
|
+
updated_at: now
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
return out;
|
|
989
|
+
})
|
|
990
|
+
);
|
|
796
991
|
}
|
|
797
992
|
async update(args) {
|
|
798
993
|
return this.runUpdate(args.where, args.data, true)[0] ?? null;
|
|
@@ -802,15 +997,36 @@ var Collection = class {
|
|
|
802
997
|
}
|
|
803
998
|
async upsert(args) {
|
|
804
999
|
this.ensureTable();
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1000
|
+
return this.guard(
|
|
1001
|
+
() => this.db.transaction(() => {
|
|
1002
|
+
const params = [];
|
|
1003
|
+
const clause = buildWhere(args.where, {
|
|
1004
|
+
params,
|
|
1005
|
+
onPath: this.trackPath,
|
|
1006
|
+
columns: this.columns
|
|
1007
|
+
});
|
|
1008
|
+
const row = this.db.prepare(`SELECT * FROM "${this.name}" WHERE ${clause} LIMIT 1`).get(...params);
|
|
1009
|
+
const now = Date.now();
|
|
1010
|
+
const recorder = this.recorder;
|
|
1011
|
+
if (row) {
|
|
1012
|
+
const current = stripSystem(this.rowToDoc(row));
|
|
1013
|
+
const updated = stripSystem(applyUpdate(current, args.update));
|
|
1014
|
+
const { setSql, values } = this.buildUpdateSet(updated, now);
|
|
1015
|
+
this.db.prepare(`UPDATE "${this.name}" SET ${setSql} WHERE _id = ?`).run(...values, row._id);
|
|
1016
|
+
recorder?.recordLocal(this.name, row._id, "upsert", now);
|
|
1017
|
+
return {
|
|
1018
|
+
...updated,
|
|
1019
|
+
_id: row._id,
|
|
1020
|
+
created_at: row.created_at,
|
|
1021
|
+
updated_at: now
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
const ins = this.buildInsert(args.create);
|
|
1025
|
+
this.db.prepare(this.insertSql()).run(...ins.values);
|
|
1026
|
+
recorder?.recordLocal(this.name, ins._id, "upsert", ins.created_at);
|
|
1027
|
+
return ins.returned;
|
|
1028
|
+
})
|
|
1029
|
+
);
|
|
814
1030
|
}
|
|
815
1031
|
/* ----------------------------- delete ----------------------------- */
|
|
816
1032
|
runDelete(where, single) {
|
|
@@ -828,12 +1044,14 @@ var Collection = class {
|
|
|
828
1044
|
const stmt = this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`);
|
|
829
1045
|
const recorder = this.recorder;
|
|
830
1046
|
const now = Date.now();
|
|
831
|
-
this.
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1047
|
+
this.guard(
|
|
1048
|
+
() => this.db.transaction(() => {
|
|
1049
|
+
for (const row of rows) {
|
|
1050
|
+
stmt.run(row._id);
|
|
1051
|
+
recorder?.recordLocal(this.name, row._id, "delete", now);
|
|
1052
|
+
}
|
|
1053
|
+
})
|
|
1054
|
+
);
|
|
837
1055
|
return rows.map((r) => this.rowToDoc(r));
|
|
838
1056
|
}
|
|
839
1057
|
async delete(args) {
|
|
@@ -845,16 +1063,30 @@ var Collection = class {
|
|
|
845
1063
|
/* --------------------------- aggregation -------------------------- */
|
|
846
1064
|
async aggregate(args = {}) {
|
|
847
1065
|
this.ensureTable();
|
|
848
|
-
return
|
|
849
|
-
|
|
850
|
-
|
|
1066
|
+
return this.guard(
|
|
1067
|
+
() => aggregate(
|
|
1068
|
+
{
|
|
1069
|
+
db: this.db,
|
|
1070
|
+
table: this.name,
|
|
1071
|
+
onPath: this.trackPath,
|
|
1072
|
+
columns: this.columns
|
|
1073
|
+
},
|
|
1074
|
+
args
|
|
1075
|
+
)
|
|
851
1076
|
);
|
|
852
1077
|
}
|
|
853
1078
|
async groupBy(args) {
|
|
854
1079
|
this.ensureTable();
|
|
855
|
-
return
|
|
856
|
-
|
|
857
|
-
|
|
1080
|
+
return this.guard(
|
|
1081
|
+
() => groupBy(
|
|
1082
|
+
{
|
|
1083
|
+
db: this.db,
|
|
1084
|
+
table: this.name,
|
|
1085
|
+
onPath: this.trackPath,
|
|
1086
|
+
columns: this.columns
|
|
1087
|
+
},
|
|
1088
|
+
args
|
|
1089
|
+
)
|
|
858
1090
|
);
|
|
859
1091
|
}
|
|
860
1092
|
};
|
|
@@ -912,15 +1144,19 @@ var AutoIndexer = class {
|
|
|
912
1144
|
};
|
|
913
1145
|
|
|
914
1146
|
// src/driver/better-sqlite3.ts
|
|
1147
|
+
var STMT_CACHE_MAX = 256;
|
|
915
1148
|
var BetterSqlite3Driver = class {
|
|
916
1149
|
name = "better-sqlite3";
|
|
917
1150
|
raw;
|
|
918
1151
|
verbose;
|
|
1152
|
+
cache = /* @__PURE__ */ new Map();
|
|
919
1153
|
constructor(BetterSqlite3, filename, options) {
|
|
920
1154
|
this.verbose = options.verbose;
|
|
921
1155
|
this.raw = new BetterSqlite3(filename, {
|
|
922
1156
|
readonly: options.readonly ?? false
|
|
923
1157
|
});
|
|
1158
|
+
this.raw.pragma("foreign_keys = ON");
|
|
1159
|
+
this.raw.pragma(`busy_timeout = ${options.busyTimeout ?? 5e3}`);
|
|
924
1160
|
if (!options.readonly && (options.wal ?? true)) {
|
|
925
1161
|
this.raw.pragma("journal_mode = WAL");
|
|
926
1162
|
}
|
|
@@ -931,21 +1167,35 @@ var BetterSqlite3Driver = class {
|
|
|
931
1167
|
}
|
|
932
1168
|
prepare(sql) {
|
|
933
1169
|
this.verbose?.(sql);
|
|
934
|
-
|
|
1170
|
+
const cached = this.cache.get(sql);
|
|
1171
|
+
if (cached) return cached;
|
|
1172
|
+
const stmt = this.raw.prepare(sql);
|
|
1173
|
+
this.cacheStmt(sql, stmt);
|
|
1174
|
+
return stmt;
|
|
1175
|
+
}
|
|
1176
|
+
cacheStmt(sql, stmt) {
|
|
1177
|
+
if (this.cache.size >= STMT_CACHE_MAX) {
|
|
1178
|
+
const oldest = this.cache.keys().next().value;
|
|
1179
|
+
if (oldest !== void 0) this.cache.delete(oldest);
|
|
1180
|
+
}
|
|
1181
|
+
this.cache.set(sql, stmt);
|
|
935
1182
|
}
|
|
936
1183
|
transaction(fn) {
|
|
937
1184
|
return this.raw.transaction(fn)();
|
|
938
1185
|
}
|
|
939
1186
|
close() {
|
|
1187
|
+
this.cache.clear();
|
|
940
1188
|
this.raw.close();
|
|
941
1189
|
}
|
|
942
1190
|
};
|
|
943
1191
|
|
|
944
1192
|
// src/driver/node-sqlite.ts
|
|
1193
|
+
var STMT_CACHE_MAX2 = 256;
|
|
945
1194
|
var NodeSqliteDriver = class {
|
|
946
1195
|
name = "node:sqlite";
|
|
947
1196
|
raw;
|
|
948
1197
|
verbose;
|
|
1198
|
+
cache = /* @__PURE__ */ new Map();
|
|
949
1199
|
depth = 0;
|
|
950
1200
|
constructor(nodeSqlite, filename, options) {
|
|
951
1201
|
this.verbose = options.verbose;
|
|
@@ -953,6 +1203,7 @@ var NodeSqliteDriver = class {
|
|
|
953
1203
|
this.raw = new DatabaseSync(filename, {
|
|
954
1204
|
readOnly: options.readonly ?? false
|
|
955
1205
|
});
|
|
1206
|
+
this.raw.exec(`PRAGMA busy_timeout = ${options.busyTimeout ?? 5e3}`);
|
|
956
1207
|
if (!options.readonly && (options.wal ?? true)) {
|
|
957
1208
|
this.raw.exec("PRAGMA journal_mode = WAL");
|
|
958
1209
|
}
|
|
@@ -963,12 +1214,20 @@ var NodeSqliteDriver = class {
|
|
|
963
1214
|
}
|
|
964
1215
|
prepare(sql) {
|
|
965
1216
|
this.verbose?.(sql);
|
|
1217
|
+
const cached = this.cache.get(sql);
|
|
1218
|
+
if (cached) return cached;
|
|
966
1219
|
const stmt = this.raw.prepare(sql);
|
|
967
|
-
|
|
1220
|
+
const wrapped = {
|
|
968
1221
|
run: (...p) => stmt.run(...p),
|
|
969
1222
|
get: (...p) => stmt.get(...p),
|
|
970
1223
|
all: (...p) => stmt.all(...p)
|
|
971
1224
|
};
|
|
1225
|
+
if (this.cache.size >= STMT_CACHE_MAX2) {
|
|
1226
|
+
const oldest = this.cache.keys().next().value;
|
|
1227
|
+
if (oldest !== void 0) this.cache.delete(oldest);
|
|
1228
|
+
}
|
|
1229
|
+
this.cache.set(sql, wrapped);
|
|
1230
|
+
return wrapped;
|
|
972
1231
|
}
|
|
973
1232
|
transaction(fn) {
|
|
974
1233
|
const savepoint = `monlite_sp_${this.depth}`;
|
|
@@ -983,12 +1242,21 @@ var NodeSqliteDriver = class {
|
|
|
983
1242
|
return result;
|
|
984
1243
|
} catch (err) {
|
|
985
1244
|
this.depth--;
|
|
986
|
-
|
|
987
|
-
|
|
1245
|
+
try {
|
|
1246
|
+
if (this.depth === 0) this.raw.exec("ROLLBACK");
|
|
1247
|
+
else this.raw.exec(`ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`);
|
|
1248
|
+
} catch {
|
|
1249
|
+
this.depth = 0;
|
|
1250
|
+
try {
|
|
1251
|
+
this.raw.exec("ROLLBACK");
|
|
1252
|
+
} catch {
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
988
1255
|
throw err;
|
|
989
1256
|
}
|
|
990
1257
|
}
|
|
991
1258
|
close() {
|
|
1259
|
+
this.cache.clear();
|
|
992
1260
|
this.raw.close();
|
|
993
1261
|
}
|
|
994
1262
|
};
|
|
@@ -1041,8 +1309,10 @@ function createDriver(filename, options = {}) {
|
|
|
1041
1309
|
|
|
1042
1310
|
// src/sync/version.ts
|
|
1043
1311
|
var TS_WIDTH = 15;
|
|
1044
|
-
|
|
1045
|
-
|
|
1312
|
+
var SEQ_WIDTH = 12;
|
|
1313
|
+
function makeVersion(ts, nodeId, seq) {
|
|
1314
|
+
const base = String(ts).padStart(TS_WIDTH, "0") + ":" + nodeId;
|
|
1315
|
+
return seq === void 0 ? base : base + ":" + String(seq).padStart(SEQ_WIDTH, "0");
|
|
1046
1316
|
}
|
|
1047
1317
|
function compareVersions(a, b) {
|
|
1048
1318
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -1062,13 +1332,16 @@ function stripSystem2(obj) {
|
|
|
1062
1332
|
return rest;
|
|
1063
1333
|
}
|
|
1064
1334
|
var SyncStore = class {
|
|
1065
|
-
constructor(db, nodeId) {
|
|
1335
|
+
constructor(db, nodeId, mon) {
|
|
1066
1336
|
this.db = db;
|
|
1337
|
+
this.mon = mon;
|
|
1067
1338
|
this.init();
|
|
1068
1339
|
this.nodeId = this.resolveNodeId(nodeId);
|
|
1069
1340
|
}
|
|
1070
1341
|
db;
|
|
1342
|
+
mon;
|
|
1071
1343
|
nodeId;
|
|
1344
|
+
versionSeq = 0;
|
|
1072
1345
|
init() {
|
|
1073
1346
|
this.db.exec(`
|
|
1074
1347
|
CREATE TABLE IF NOT EXISTS _monlite_changes (
|
|
@@ -1101,7 +1374,9 @@ var SyncStore = class {
|
|
|
1101
1374
|
}
|
|
1102
1375
|
resolveNodeId(explicit) {
|
|
1103
1376
|
if (explicit) {
|
|
1104
|
-
this.db.prepare(
|
|
1377
|
+
this.db.prepare(
|
|
1378
|
+
`INSERT OR REPLACE INTO _monlite_meta (key, value) VALUES ('nodeId', ?)`
|
|
1379
|
+
).run(explicit);
|
|
1105
1380
|
return explicit;
|
|
1106
1381
|
}
|
|
1107
1382
|
const row = this.db.prepare(`SELECT value FROM _monlite_meta WHERE key = 'nodeId'`).get();
|
|
@@ -1117,7 +1392,7 @@ var SyncStore = class {
|
|
|
1117
1392
|
/* ----------------------- local change recording ----------------------- */
|
|
1118
1393
|
/** Append a locally-originated change to the feed. Call inside a write txn. */
|
|
1119
1394
|
recordLocal(collection, id, op, ts) {
|
|
1120
|
-
const version = makeVersion(ts, this.nodeId);
|
|
1395
|
+
const version = makeVersion(ts, this.nodeId, this.versionSeq++);
|
|
1121
1396
|
this.db.prepare(
|
|
1122
1397
|
`INSERT INTO _monlite_changes (coll, doc_id, op, version, ts, source, pushed)
|
|
1123
1398
|
VALUES (?, ?, ?, ?, ?, 'local', 0)`
|
|
@@ -1134,13 +1409,18 @@ var SyncStore = class {
|
|
|
1134
1409
|
}
|
|
1135
1410
|
/* ----------------------------- push side ----------------------------- */
|
|
1136
1411
|
/** Latest unpushed local change per document (the push queue). */
|
|
1137
|
-
pending(collections) {
|
|
1412
|
+
pending(collections, limit) {
|
|
1138
1413
|
const params = [];
|
|
1139
1414
|
let collFilter = "";
|
|
1140
1415
|
if (collections && collections.length) {
|
|
1141
1416
|
collFilter = ` AND coll IN (${collections.map(() => "?").join(", ")})`;
|
|
1142
1417
|
params.push(...collections);
|
|
1143
1418
|
}
|
|
1419
|
+
let limitClause = "";
|
|
1420
|
+
if (limit != null && limit > 0) {
|
|
1421
|
+
limitClause = " LIMIT ?";
|
|
1422
|
+
params.push(limit);
|
|
1423
|
+
}
|
|
1144
1424
|
const rows = this.db.prepare(
|
|
1145
1425
|
`SELECT c.seq, c.coll, c.doc_id, c.op, c.version, c.ts
|
|
1146
1426
|
FROM _monlite_changes c
|
|
@@ -1150,7 +1430,7 @@ var SyncStore = class {
|
|
|
1150
1430
|
WHERE source = 'local' AND pushed = 0${collFilter}
|
|
1151
1431
|
GROUP BY coll, doc_id
|
|
1152
1432
|
) m ON c.coll = m.coll AND c.doc_id = m.doc_id AND c.seq = m.mseq
|
|
1153
|
-
ORDER BY c.seq`
|
|
1433
|
+
ORDER BY c.seq${limitClause}`
|
|
1154
1434
|
).all(...params);
|
|
1155
1435
|
return rows.map((r) => {
|
|
1156
1436
|
const change = {
|
|
@@ -1188,31 +1468,34 @@ var SyncStore = class {
|
|
|
1188
1468
|
*/
|
|
1189
1469
|
applyRemote(change, resolver) {
|
|
1190
1470
|
assertName(change.collection);
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1471
|
+
return this.db.transaction(() => {
|
|
1472
|
+
const localVersion = this.currentVersion(change.collection, change._id);
|
|
1473
|
+
let winner;
|
|
1474
|
+
if (localVersion === null) {
|
|
1475
|
+
winner = "remote";
|
|
1476
|
+
} else if (change.version === localVersion) {
|
|
1477
|
+
return { applied: false, conflict: false, winner: "none" };
|
|
1478
|
+
} else {
|
|
1479
|
+
winner = resolver ? resolver({
|
|
1480
|
+
collection: change.collection,
|
|
1481
|
+
_id: change._id,
|
|
1482
|
+
local: { version: localVersion },
|
|
1483
|
+
remote: { version: change.version, doc: change.doc }
|
|
1484
|
+
}) : compareVersions(change.version, localVersion) > 0 ? "remote" : "local";
|
|
1485
|
+
this.recordConflict(
|
|
1486
|
+
change.collection,
|
|
1487
|
+
change._id,
|
|
1488
|
+
localVersion,
|
|
1489
|
+
change.version,
|
|
1490
|
+
winner
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
if (winner !== "remote") {
|
|
1494
|
+
if (localVersion !== null) {
|
|
1495
|
+
this.recordLocal(change.collection, change._id, "upsert", Date.now());
|
|
1496
|
+
}
|
|
1497
|
+
return { applied: false, conflict: localVersion !== null, winner };
|
|
1498
|
+
}
|
|
1216
1499
|
this.applyData(change);
|
|
1217
1500
|
this.db.prepare(
|
|
1218
1501
|
`INSERT INTO _monlite_changes (coll, doc_id, op, version, ts, source, pushed)
|
|
@@ -1224,37 +1507,49 @@ var SyncStore = class {
|
|
|
1224
1507
|
change.version,
|
|
1225
1508
|
versionTs(change.version)
|
|
1226
1509
|
);
|
|
1510
|
+
return {
|
|
1511
|
+
applied: true,
|
|
1512
|
+
conflict: localVersion !== null,
|
|
1513
|
+
winner: "remote"
|
|
1514
|
+
};
|
|
1227
1515
|
});
|
|
1228
|
-
return { applied: true, conflict: localVersion !== null, winner: "remote" };
|
|
1229
1516
|
}
|
|
1230
1517
|
applyData(change) {
|
|
1231
|
-
const
|
|
1518
|
+
const ts = versionTs(change.version);
|
|
1519
|
+
if (this.mon) {
|
|
1520
|
+
this.mon.collection(change.collection).applyRemoteWrite(change.op, change._id, change.doc, ts);
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const coll = change.collection;
|
|
1232
1524
|
this.ensureCollTable(coll);
|
|
1233
|
-
if (op === "delete") {
|
|
1234
|
-
this.db.prepare(`DELETE FROM "${coll}" WHERE _id = ?`).run(_id);
|
|
1525
|
+
if (change.op === "delete") {
|
|
1526
|
+
this.db.prepare(`DELETE FROM "${coll}" WHERE _id = ?`).run(change._id);
|
|
1235
1527
|
return;
|
|
1236
1528
|
}
|
|
1237
1529
|
const doc = change.doc ?? {};
|
|
1238
|
-
const data = JSON.stringify(stripSystem2(doc));
|
|
1239
|
-
const ts = versionTs(change.version);
|
|
1240
1530
|
const createdAt = typeof doc.created_at === "number" ? doc.created_at : ts;
|
|
1241
1531
|
this.db.prepare(
|
|
1242
1532
|
`INSERT INTO "${coll}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
1243
1533
|
ON CONFLICT(_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
|
|
1244
|
-
).run(_id,
|
|
1534
|
+
).run(change._id, JSON.stringify(stripSystem2(doc)), createdAt, ts);
|
|
1245
1535
|
}
|
|
1246
1536
|
/**
|
|
1247
1537
|
* Latest change per document with `seq` greater than the given watermark,
|
|
1248
1538
|
* as RemoteChanges (used when this database acts as a sync *source*, e.g. the
|
|
1249
1539
|
* monlite-as-remote adapter). Returns the new watermark to resume from.
|
|
1250
1540
|
*/
|
|
1251
|
-
changesSince(seq, collections) {
|
|
1541
|
+
changesSince(seq, collections, limit) {
|
|
1252
1542
|
const params = [seq];
|
|
1253
1543
|
let collFilter = "";
|
|
1254
1544
|
if (collections && collections.length) {
|
|
1255
1545
|
collFilter = ` AND coll IN (${collections.map(() => "?").join(", ")})`;
|
|
1256
1546
|
params.push(...collections);
|
|
1257
1547
|
}
|
|
1548
|
+
let limitClause = "";
|
|
1549
|
+
if (limit != null && limit > 0) {
|
|
1550
|
+
limitClause = " LIMIT ?";
|
|
1551
|
+
params.push(limit);
|
|
1552
|
+
}
|
|
1258
1553
|
const rows = this.db.prepare(
|
|
1259
1554
|
`SELECT c.seq, c.coll, c.doc_id, c.op, c.version, c.ts
|
|
1260
1555
|
FROM _monlite_changes c
|
|
@@ -1264,7 +1559,7 @@ var SyncStore = class {
|
|
|
1264
1559
|
WHERE seq > ?${collFilter}
|
|
1265
1560
|
GROUP BY coll, doc_id
|
|
1266
1561
|
) m ON c.coll = m.coll AND c.doc_id = m.doc_id AND c.seq = m.mseq
|
|
1267
|
-
ORDER BY c.seq`
|
|
1562
|
+
ORDER BY c.seq${limitClause}`
|
|
1268
1563
|
).all(...params);
|
|
1269
1564
|
const changes = rows.map((r) => {
|
|
1270
1565
|
const change = {
|
|
@@ -1280,8 +1575,8 @@ var SyncStore = class {
|
|
|
1280
1575
|
}
|
|
1281
1576
|
return change;
|
|
1282
1577
|
});
|
|
1283
|
-
const
|
|
1284
|
-
return { changes, maxSeq
|
|
1578
|
+
const maxSeq = rows.length ? rows[rows.length - 1].seq : seq;
|
|
1579
|
+
return { changes, maxSeq };
|
|
1285
1580
|
}
|
|
1286
1581
|
/* ------------------------------ bootstrap ----------------------------- */
|
|
1287
1582
|
/**
|
|
@@ -1293,6 +1588,7 @@ var SyncStore = class {
|
|
|
1293
1588
|
this.db.transaction(() => {
|
|
1294
1589
|
for (const coll of collections) {
|
|
1295
1590
|
assertName(coll);
|
|
1591
|
+
if (!this.tableExists(coll)) continue;
|
|
1296
1592
|
const docs = this.db.prepare(`SELECT _id, updated_at FROM "${coll}"`).all();
|
|
1297
1593
|
for (const d of docs) {
|
|
1298
1594
|
if (this.currentVersion(coll, d._id) !== null) continue;
|
|
@@ -1338,7 +1634,14 @@ var SyncStore = class {
|
|
|
1338
1634
|
this.db.prepare(
|
|
1339
1635
|
`INSERT INTO _monlite_conflicts (coll, doc_id, local_version, remote_version, winner, ts)
|
|
1340
1636
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1341
|
-
).run(
|
|
1637
|
+
).run(
|
|
1638
|
+
coll,
|
|
1639
|
+
id,
|
|
1640
|
+
localVersion,
|
|
1641
|
+
remoteVersion,
|
|
1642
|
+
winner,
|
|
1643
|
+
versionTs(remoteVersion)
|
|
1644
|
+
);
|
|
1342
1645
|
}
|
|
1343
1646
|
conflicts() {
|
|
1344
1647
|
const rows = this.db.prepare(
|
|
@@ -1357,6 +1660,8 @@ var SyncStore = class {
|
|
|
1357
1660
|
/* ------------------------------ helpers ------------------------------- */
|
|
1358
1661
|
readDoc(coll, id) {
|
|
1359
1662
|
assertName(coll);
|
|
1663
|
+
if (this.mon) return this.mon.collection(coll).getRaw(id);
|
|
1664
|
+
if (!this.tableExists(coll)) return null;
|
|
1360
1665
|
const row = this.db.prepare(
|
|
1361
1666
|
`SELECT _id, data, created_at, updated_at FROM "${coll}" WHERE _id = ?`
|
|
1362
1667
|
).get(id);
|
|
@@ -1367,6 +1672,9 @@ var SyncStore = class {
|
|
|
1367
1672
|
doc.updated_at = row.updated_at;
|
|
1368
1673
|
return doc;
|
|
1369
1674
|
}
|
|
1675
|
+
tableExists(name) {
|
|
1676
|
+
return this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?`).get(name) != null;
|
|
1677
|
+
}
|
|
1370
1678
|
ensureCollTable(coll) {
|
|
1371
1679
|
assertName(coll);
|
|
1372
1680
|
this.db.exec(
|
|
@@ -1414,6 +1722,7 @@ var Monlite = class {
|
|
|
1414
1722
|
driver: options.driver,
|
|
1415
1723
|
readonly: options.readonly,
|
|
1416
1724
|
wal: options.wal,
|
|
1725
|
+
busyTimeout: options.busyTimeout,
|
|
1417
1726
|
verbose: options.verbose
|
|
1418
1727
|
});
|
|
1419
1728
|
this.autoIndexer = new AutoIndexer(
|
|
@@ -1422,7 +1731,7 @@ var Monlite = class {
|
|
|
1422
1731
|
options.autoIndexAfter ?? 10
|
|
1423
1732
|
);
|
|
1424
1733
|
if (options.sync) {
|
|
1425
|
-
this.$sync = new SyncStore(this.driver, options.nodeId);
|
|
1734
|
+
this.$sync = new SyncStore(this.driver, options.nodeId, this);
|
|
1426
1735
|
}
|
|
1427
1736
|
}
|
|
1428
1737
|
/** Stable node id for LWW tie-breaking (only when sync is enabled). */
|
|
@@ -1449,6 +1758,15 @@ var Monlite = class {
|
|
|
1449
1758
|
if (!col) {
|
|
1450
1759
|
col = new Collection(this, name, options);
|
|
1451
1760
|
this.collections.set(name, col);
|
|
1761
|
+
} else if (options?.schema) {
|
|
1762
|
+
const requested = Object.keys(options.schema);
|
|
1763
|
+
const existing = new Set(col.columnNames);
|
|
1764
|
+
const sameShape = col.mode === "structured" && requested.length === existing.size && requested.every((c) => existing.has(c));
|
|
1765
|
+
if (!sameShape) {
|
|
1766
|
+
throw new MonliteError(
|
|
1767
|
+
`Collection "${name}" was already opened with a different schema/mode. A collection's storage mode is fixed on first access.`
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1452
1770
|
}
|
|
1453
1771
|
return col;
|
|
1454
1772
|
}
|
|
@@ -1483,7 +1801,11 @@ var Monlite = class {
|
|
|
1483
1801
|
$executeRaw(strings, ...values) {
|
|
1484
1802
|
this.assertOpen();
|
|
1485
1803
|
const { sql, params } = buildTagged(strings, values);
|
|
1486
|
-
|
|
1804
|
+
try {
|
|
1805
|
+
return Promise.resolve(this.driver.prepare(sql).run(...params).changes);
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
throw normalizeDriverError(err);
|
|
1808
|
+
}
|
|
1487
1809
|
}
|
|
1488
1810
|
/** Like {@link $executeRaw} but takes a raw SQL string and positional params. */
|
|
1489
1811
|
$executeRawUnsafe(sql, ...params) {
|
|
@@ -1543,13 +1865,18 @@ function createDb(filename, options) {
|
|
|
1543
1865
|
|
|
1544
1866
|
exports.Collection = Collection;
|
|
1545
1867
|
exports.Monlite = Monlite;
|
|
1868
|
+
exports.MonliteConstraintError = MonliteConstraintError;
|
|
1546
1869
|
exports.MonliteError = MonliteError;
|
|
1870
|
+
exports.MonliteForeignKeyError = MonliteForeignKeyError;
|
|
1871
|
+
exports.MonliteNotNullError = MonliteNotNullError;
|
|
1547
1872
|
exports.MonliteQueryError = MonliteQueryError;
|
|
1873
|
+
exports.MonliteUniqueConstraintError = MonliteUniqueConstraintError;
|
|
1548
1874
|
exports.SyncStore = SyncStore;
|
|
1549
1875
|
exports.compareVersions = compareVersions;
|
|
1550
1876
|
exports.createDb = createDb;
|
|
1551
1877
|
exports.isObjectId = isObjectId;
|
|
1552
1878
|
exports.makeVersion = makeVersion;
|
|
1879
|
+
exports.normalizeDriverError = normalizeDriverError;
|
|
1553
1880
|
exports.objectId = objectId;
|
|
1554
1881
|
exports.versionTs = versionTs;
|
|
1555
1882
|
//# sourceMappingURL=index.cjs.map
|