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