@monlite/core 0.5.0 → 0.6.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 +92 -12
- package/dist/index.cjs +331 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -6
- package/dist/index.d.ts +66 -6
- package/dist/index.js +327 -111
- package/dist/index.js.map +1 -1
- package/package.json +1 -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));
|
|
@@ -195,15 +250,17 @@ function inExpr(expr, arr, ctx, negate) {
|
|
|
195
250
|
}).join(", ");
|
|
196
251
|
return negate ? `(${expr} IS NULL OR ${expr} NOT IN (${placeholders}))` : `${expr} IN (${placeholders})`;
|
|
197
252
|
}
|
|
198
|
-
function containsExpr(field, expr, v, ctx) {
|
|
253
|
+
function containsExpr(field, expr, v, ctx, ci) {
|
|
254
|
+
const sub = ci ? `instr(lower(${expr}), lower(?)) > 0` : `instr(${expr}, ?) > 0`;
|
|
199
255
|
if (isColumn(field, ctx.columns)) {
|
|
200
256
|
ctx.params.push(bindable(v));
|
|
201
|
-
return
|
|
257
|
+
return sub;
|
|
202
258
|
}
|
|
203
259
|
const path = pathLiteral(field);
|
|
260
|
+
const member = ci ? `lower(value) = lower(?)` : `value = ?`;
|
|
204
261
|
ctx.params.push(bindable(v));
|
|
205
262
|
ctx.params.push(bindable(v));
|
|
206
|
-
return `(CASE WHEN json_type(data, ${path}) = 'array' THEN EXISTS (SELECT 1 FROM json_each(data, ${path}) WHERE
|
|
263
|
+
return `(CASE WHEN json_type(data, ${path}) = 'array' THEN EXISTS (SELECT 1 FROM json_each(data, ${path}) WHERE ${member}) ELSE ${sub} END)`;
|
|
207
264
|
}
|
|
208
265
|
function hasExpr(field, expr, v, ctx) {
|
|
209
266
|
ctx.params.push(bindable(v));
|
|
@@ -236,16 +293,26 @@ function buildOrderBy(orderBy, onPath, columns) {
|
|
|
236
293
|
}
|
|
237
294
|
|
|
238
295
|
// src/query/path.ts
|
|
296
|
+
var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
297
|
+
function safeSegments(path) {
|
|
298
|
+
const segs = path.split(".");
|
|
299
|
+
for (const seg of segs) {
|
|
300
|
+
if (FORBIDDEN.has(seg)) {
|
|
301
|
+
throw new MonliteQueryError(`Illegal path segment "${seg}" in "${path}"`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return segs;
|
|
305
|
+
}
|
|
239
306
|
function getPath(obj, path) {
|
|
240
307
|
let cur = obj;
|
|
241
|
-
for (const seg of path
|
|
308
|
+
for (const seg of safeSegments(path)) {
|
|
242
309
|
if (cur == null) return void 0;
|
|
243
310
|
cur = cur[seg];
|
|
244
311
|
}
|
|
245
312
|
return cur;
|
|
246
313
|
}
|
|
247
314
|
function setPath(obj, path, value) {
|
|
248
|
-
const segs = path
|
|
315
|
+
const segs = safeSegments(path);
|
|
249
316
|
let cur = obj;
|
|
250
317
|
for (let i = 0; i < segs.length - 1; i++) {
|
|
251
318
|
const seg = segs[i];
|
|
@@ -255,7 +322,7 @@ function setPath(obj, path, value) {
|
|
|
255
322
|
cur[segs[segs.length - 1]] = value;
|
|
256
323
|
}
|
|
257
324
|
function unsetPath(obj, path) {
|
|
258
|
-
const segs = path
|
|
325
|
+
const segs = safeSegments(path);
|
|
259
326
|
let cur = obj;
|
|
260
327
|
for (let i = 0; i < segs.length - 1; i++) {
|
|
261
328
|
const seg = segs[i];
|
|
@@ -308,8 +375,13 @@ function applyUpdate(doc, data) {
|
|
|
308
375
|
}
|
|
309
376
|
if (ops.$inc) {
|
|
310
377
|
for (const [path, by] of Object.entries(ops.$inc)) {
|
|
378
|
+
if (typeof by !== "number" || !Number.isFinite(by)) {
|
|
379
|
+
throw new MonliteQueryError(
|
|
380
|
+
`$inc on "${path}" requires a finite number, got ${JSON.stringify(by)}`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
311
383
|
const cur = getPath(next, path);
|
|
312
|
-
setPath(next, path, (typeof cur === "number" ? cur : 0) +
|
|
384
|
+
setPath(next, path, (typeof cur === "number" ? cur : 0) + by);
|
|
313
385
|
}
|
|
314
386
|
}
|
|
315
387
|
if (ops.$push) {
|
|
@@ -428,7 +500,7 @@ function buildHaving(having, params, columns) {
|
|
|
428
500
|
}
|
|
429
501
|
function groupBy(ctx, args) {
|
|
430
502
|
if (!Array.isArray(args.by) || args.by.length === 0) {
|
|
431
|
-
throw new
|
|
503
|
+
throw new MonliteQueryError("groupBy requires a non-empty `by` array");
|
|
432
504
|
}
|
|
433
505
|
const params = [];
|
|
434
506
|
const where = buildWhere(args.where, {
|
|
@@ -437,13 +509,16 @@ function groupBy(ctx, args) {
|
|
|
437
509
|
columns: ctx.columns
|
|
438
510
|
});
|
|
439
511
|
const groupExprs = [];
|
|
512
|
+
const groupCols = [];
|
|
440
513
|
const selects = [];
|
|
441
|
-
|
|
514
|
+
args.by.forEach((field, gi) => {
|
|
442
515
|
if (!isColumn(field, ctx.columns)) ctx.onPath(field);
|
|
443
516
|
const expr = fieldExpr(field, ctx.columns);
|
|
517
|
+
const alias = `grp_${gi}`;
|
|
444
518
|
groupExprs.push(expr);
|
|
445
|
-
selects.push(`${expr} AS
|
|
446
|
-
|
|
519
|
+
selects.push(`${expr} AS ${alias}`);
|
|
520
|
+
groupCols.push({ alias, field });
|
|
521
|
+
});
|
|
447
522
|
selects.push(`COUNT(*) AS agg_count`);
|
|
448
523
|
const { selects: accSelects, cols } = buildAccumulators(args, ctx.onPath, ctx.columns);
|
|
449
524
|
selects.push(...accSelects);
|
|
@@ -472,7 +547,7 @@ function groupBy(ctx, args) {
|
|
|
472
547
|
const rows = ctx.db.prepare(sql).all(...params);
|
|
473
548
|
return rows.map((row) => {
|
|
474
549
|
const out = {};
|
|
475
|
-
for (const field of
|
|
550
|
+
for (const { alias, field } of groupCols) out[field] = row[alias];
|
|
476
551
|
if (args._count) out._count = row.agg_count;
|
|
477
552
|
for (const col of cols) {
|
|
478
553
|
(out[col.kind] ??= {})[col.field] = row[col.alias] ?? null;
|
|
@@ -511,6 +586,13 @@ var Collection = class {
|
|
|
511
586
|
);
|
|
512
587
|
}
|
|
513
588
|
const normalized = typeof def === "string" ? { type: def } : def;
|
|
589
|
+
if (normalized.references && !/^[A-Za-z_][A-Za-z0-9_]*(\([A-Za-z_][A-Za-z0-9_]*\))?$/.test(
|
|
590
|
+
normalized.references
|
|
591
|
+
)) {
|
|
592
|
+
throw new MonliteError(
|
|
593
|
+
`Invalid references "${normalized.references}" on column "${field}"`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
514
596
|
this.columnDefs[field] = normalized;
|
|
515
597
|
this.columnOrder.push(field);
|
|
516
598
|
this.columns.add(field);
|
|
@@ -532,6 +614,14 @@ var Collection = class {
|
|
|
532
614
|
get db() {
|
|
533
615
|
return this.mon.driver;
|
|
534
616
|
}
|
|
617
|
+
/** Run a DB operation, normalizing driver errors into typed MonliteErrors. */
|
|
618
|
+
guard(fn) {
|
|
619
|
+
try {
|
|
620
|
+
return fn();
|
|
621
|
+
} catch (err) {
|
|
622
|
+
throw normalizeDriverError(err, this.name);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
535
625
|
/** Native column names declared for this collection (structured mode). */
|
|
536
626
|
get columnNames() {
|
|
537
627
|
return [...this.columnOrder];
|
|
@@ -584,7 +674,11 @@ var Collection = class {
|
|
|
584
674
|
if (this.mode === "structured") {
|
|
585
675
|
for (const field of this.columnOrder) {
|
|
586
676
|
const value = row[field];
|
|
587
|
-
if (value ===
|
|
677
|
+
if (value === void 0) continue;
|
|
678
|
+
if (value === null) {
|
|
679
|
+
doc[field] = null;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
588
682
|
doc[field] = this.jsonColumns.has(field) ? JSON.parse(value) : value;
|
|
589
683
|
}
|
|
590
684
|
}
|
|
@@ -597,6 +691,11 @@ var Collection = class {
|
|
|
597
691
|
if (this.jsonColumns.has(field)) {
|
|
598
692
|
return value === void 0 ? null : JSON.stringify(value);
|
|
599
693
|
}
|
|
694
|
+
if (value !== null && typeof value === "object" && !(value instanceof Date) && !Buffer.isBuffer(value)) {
|
|
695
|
+
throw new MonliteQueryError(
|
|
696
|
+
`Column "${field}" cannot store an object/array. Declare it as { type: "JSON" } to store structured values.`
|
|
697
|
+
);
|
|
698
|
+
}
|
|
600
699
|
return bindable(value);
|
|
601
700
|
}
|
|
602
701
|
insertColumns() {
|
|
@@ -679,21 +778,22 @@ var Collection = class {
|
|
|
679
778
|
this.db.prepare(this.insertSql()).run(...row.values);
|
|
680
779
|
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
681
780
|
};
|
|
682
|
-
|
|
683
|
-
else write();
|
|
781
|
+
this.guard(() => recorder ? this.db.transaction(write) : write());
|
|
684
782
|
return row.returned;
|
|
685
783
|
}
|
|
686
784
|
async createMany(args) {
|
|
687
785
|
this.ensureTable();
|
|
688
786
|
const stmt = this.db.prepare(this.insertSql());
|
|
689
787
|
const recorder = this.recorder;
|
|
690
|
-
this.
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
788
|
+
this.guard(
|
|
789
|
+
() => this.db.transaction(() => {
|
|
790
|
+
for (const item of args.data) {
|
|
791
|
+
const row = this.buildInsert(item);
|
|
792
|
+
stmt.run(...row.values);
|
|
793
|
+
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
);
|
|
697
797
|
return { count: args.data.length };
|
|
698
798
|
}
|
|
699
799
|
/* ------------------------------ read ------------------------------ */
|
|
@@ -723,6 +823,28 @@ var Collection = class {
|
|
|
723
823
|
const rows = await this.findMany({ ...args, take: 1 });
|
|
724
824
|
return rows[0] ?? null;
|
|
725
825
|
}
|
|
826
|
+
/** Alias of {@link findFirst} for Prisma familiarity. */
|
|
827
|
+
async findUnique(args = {}) {
|
|
828
|
+
return this.findFirst(args);
|
|
829
|
+
}
|
|
830
|
+
/** Like {@link findFirst} but throws if no document matches. */
|
|
831
|
+
async findFirstOrThrow(args = {}) {
|
|
832
|
+
const doc = await this.findFirst(args);
|
|
833
|
+
if (!doc) throw new MonliteError(`No document found in "${this.name}"`);
|
|
834
|
+
return doc;
|
|
835
|
+
}
|
|
836
|
+
/** True if at least one document matches. */
|
|
837
|
+
async exists(where) {
|
|
838
|
+
this.ensureTable();
|
|
839
|
+
const params = [];
|
|
840
|
+
const clause = buildWhere(where, {
|
|
841
|
+
params,
|
|
842
|
+
onPath: this.trackPath,
|
|
843
|
+
columns: this.columns
|
|
844
|
+
});
|
|
845
|
+
const row = this.db.prepare(`SELECT 1 FROM "${this.name}" WHERE ${clause} LIMIT 1`).get(...params);
|
|
846
|
+
return row != null;
|
|
847
|
+
}
|
|
726
848
|
async findById(id) {
|
|
727
849
|
this.ensureTable();
|
|
728
850
|
const row = this.db.prepare(`SELECT * FROM "${this.name}" WHERE _id = ?`).get(id);
|
|
@@ -758,8 +880,10 @@ var Collection = class {
|
|
|
758
880
|
this.trackPath(field);
|
|
759
881
|
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
882
|
}
|
|
761
|
-
|
|
762
|
-
|
|
883
|
+
return this.guard(() => {
|
|
884
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
885
|
+
return rows.map((r) => r.v);
|
|
886
|
+
});
|
|
763
887
|
}
|
|
764
888
|
/* ----------------------------- update ----------------------------- */
|
|
765
889
|
runUpdate(where, data, single) {
|
|
@@ -776,23 +900,25 @@ var Collection = class {
|
|
|
776
900
|
if (!rows.length) return [];
|
|
777
901
|
const now = Date.now();
|
|
778
902
|
const recorder = this.recorder;
|
|
779
|
-
return this.
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
903
|
+
return this.guard(
|
|
904
|
+
() => this.db.transaction(() => {
|
|
905
|
+
const out = [];
|
|
906
|
+
for (const row of rows) {
|
|
907
|
+
const current = stripSystem(this.rowToDoc(row));
|
|
908
|
+
const updated = stripSystem(applyUpdate(current, data));
|
|
909
|
+
const { setSql, values } = this.buildUpdateSet(updated, now);
|
|
910
|
+
this.db.prepare(`UPDATE "${this.name}" SET ${setSql} WHERE _id = ?`).run(...values, row._id);
|
|
911
|
+
recorder?.recordLocal(this.name, row._id, "upsert", now);
|
|
912
|
+
out.push({
|
|
913
|
+
...updated,
|
|
914
|
+
_id: row._id,
|
|
915
|
+
created_at: row.created_at,
|
|
916
|
+
updated_at: now
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
return out;
|
|
920
|
+
})
|
|
921
|
+
);
|
|
796
922
|
}
|
|
797
923
|
async update(args) {
|
|
798
924
|
return this.runUpdate(args.where, args.data, true)[0] ?? null;
|
|
@@ -802,15 +928,36 @@ var Collection = class {
|
|
|
802
928
|
}
|
|
803
929
|
async upsert(args) {
|
|
804
930
|
this.ensureTable();
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
931
|
+
return this.guard(
|
|
932
|
+
() => this.db.transaction(() => {
|
|
933
|
+
const params = [];
|
|
934
|
+
const clause = buildWhere(args.where, {
|
|
935
|
+
params,
|
|
936
|
+
onPath: this.trackPath,
|
|
937
|
+
columns: this.columns
|
|
938
|
+
});
|
|
939
|
+
const row = this.db.prepare(`SELECT * FROM "${this.name}" WHERE ${clause} LIMIT 1`).get(...params);
|
|
940
|
+
const now = Date.now();
|
|
941
|
+
const recorder = this.recorder;
|
|
942
|
+
if (row) {
|
|
943
|
+
const current = stripSystem(this.rowToDoc(row));
|
|
944
|
+
const updated = stripSystem(applyUpdate(current, args.update));
|
|
945
|
+
const { setSql, values } = this.buildUpdateSet(updated, now);
|
|
946
|
+
this.db.prepare(`UPDATE "${this.name}" SET ${setSql} WHERE _id = ?`).run(...values, row._id);
|
|
947
|
+
recorder?.recordLocal(this.name, row._id, "upsert", now);
|
|
948
|
+
return {
|
|
949
|
+
...updated,
|
|
950
|
+
_id: row._id,
|
|
951
|
+
created_at: row.created_at,
|
|
952
|
+
updated_at: now
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
const ins = this.buildInsert(args.create);
|
|
956
|
+
this.db.prepare(this.insertSql()).run(...ins.values);
|
|
957
|
+
recorder?.recordLocal(this.name, ins._id, "upsert", ins.created_at);
|
|
958
|
+
return ins.returned;
|
|
959
|
+
})
|
|
960
|
+
);
|
|
814
961
|
}
|
|
815
962
|
/* ----------------------------- delete ----------------------------- */
|
|
816
963
|
runDelete(where, single) {
|
|
@@ -828,12 +975,14 @@ var Collection = class {
|
|
|
828
975
|
const stmt = this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`);
|
|
829
976
|
const recorder = this.recorder;
|
|
830
977
|
const now = Date.now();
|
|
831
|
-
this.
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
978
|
+
this.guard(
|
|
979
|
+
() => this.db.transaction(() => {
|
|
980
|
+
for (const row of rows) {
|
|
981
|
+
stmt.run(row._id);
|
|
982
|
+
recorder?.recordLocal(this.name, row._id, "delete", now);
|
|
983
|
+
}
|
|
984
|
+
})
|
|
985
|
+
);
|
|
837
986
|
return rows.map((r) => this.rowToDoc(r));
|
|
838
987
|
}
|
|
839
988
|
async delete(args) {
|
|
@@ -845,16 +994,20 @@ var Collection = class {
|
|
|
845
994
|
/* --------------------------- aggregation -------------------------- */
|
|
846
995
|
async aggregate(args = {}) {
|
|
847
996
|
this.ensureTable();
|
|
848
|
-
return
|
|
849
|
-
|
|
850
|
-
|
|
997
|
+
return this.guard(
|
|
998
|
+
() => aggregate(
|
|
999
|
+
{ db: this.db, table: this.name, onPath: this.trackPath, columns: this.columns },
|
|
1000
|
+
args
|
|
1001
|
+
)
|
|
851
1002
|
);
|
|
852
1003
|
}
|
|
853
1004
|
async groupBy(args) {
|
|
854
1005
|
this.ensureTable();
|
|
855
|
-
return
|
|
856
|
-
|
|
857
|
-
|
|
1006
|
+
return this.guard(
|
|
1007
|
+
() => groupBy(
|
|
1008
|
+
{ db: this.db, table: this.name, onPath: this.trackPath, columns: this.columns },
|
|
1009
|
+
args
|
|
1010
|
+
)
|
|
858
1011
|
);
|
|
859
1012
|
}
|
|
860
1013
|
};
|
|
@@ -912,15 +1065,19 @@ var AutoIndexer = class {
|
|
|
912
1065
|
};
|
|
913
1066
|
|
|
914
1067
|
// src/driver/better-sqlite3.ts
|
|
1068
|
+
var STMT_CACHE_MAX = 256;
|
|
915
1069
|
var BetterSqlite3Driver = class {
|
|
916
1070
|
name = "better-sqlite3";
|
|
917
1071
|
raw;
|
|
918
1072
|
verbose;
|
|
1073
|
+
cache = /* @__PURE__ */ new Map();
|
|
919
1074
|
constructor(BetterSqlite3, filename, options) {
|
|
920
1075
|
this.verbose = options.verbose;
|
|
921
1076
|
this.raw = new BetterSqlite3(filename, {
|
|
922
1077
|
readonly: options.readonly ?? false
|
|
923
1078
|
});
|
|
1079
|
+
this.raw.pragma("foreign_keys = ON");
|
|
1080
|
+
this.raw.pragma(`busy_timeout = ${options.busyTimeout ?? 5e3}`);
|
|
924
1081
|
if (!options.readonly && (options.wal ?? true)) {
|
|
925
1082
|
this.raw.pragma("journal_mode = WAL");
|
|
926
1083
|
}
|
|
@@ -931,21 +1088,35 @@ var BetterSqlite3Driver = class {
|
|
|
931
1088
|
}
|
|
932
1089
|
prepare(sql) {
|
|
933
1090
|
this.verbose?.(sql);
|
|
934
|
-
|
|
1091
|
+
const cached = this.cache.get(sql);
|
|
1092
|
+
if (cached) return cached;
|
|
1093
|
+
const stmt = this.raw.prepare(sql);
|
|
1094
|
+
this.cacheStmt(sql, stmt);
|
|
1095
|
+
return stmt;
|
|
1096
|
+
}
|
|
1097
|
+
cacheStmt(sql, stmt) {
|
|
1098
|
+
if (this.cache.size >= STMT_CACHE_MAX) {
|
|
1099
|
+
const oldest = this.cache.keys().next().value;
|
|
1100
|
+
if (oldest !== void 0) this.cache.delete(oldest);
|
|
1101
|
+
}
|
|
1102
|
+
this.cache.set(sql, stmt);
|
|
935
1103
|
}
|
|
936
1104
|
transaction(fn) {
|
|
937
1105
|
return this.raw.transaction(fn)();
|
|
938
1106
|
}
|
|
939
1107
|
close() {
|
|
1108
|
+
this.cache.clear();
|
|
940
1109
|
this.raw.close();
|
|
941
1110
|
}
|
|
942
1111
|
};
|
|
943
1112
|
|
|
944
1113
|
// src/driver/node-sqlite.ts
|
|
1114
|
+
var STMT_CACHE_MAX2 = 256;
|
|
945
1115
|
var NodeSqliteDriver = class {
|
|
946
1116
|
name = "node:sqlite";
|
|
947
1117
|
raw;
|
|
948
1118
|
verbose;
|
|
1119
|
+
cache = /* @__PURE__ */ new Map();
|
|
949
1120
|
depth = 0;
|
|
950
1121
|
constructor(nodeSqlite, filename, options) {
|
|
951
1122
|
this.verbose = options.verbose;
|
|
@@ -953,6 +1124,7 @@ var NodeSqliteDriver = class {
|
|
|
953
1124
|
this.raw = new DatabaseSync(filename, {
|
|
954
1125
|
readOnly: options.readonly ?? false
|
|
955
1126
|
});
|
|
1127
|
+
this.raw.exec(`PRAGMA busy_timeout = ${options.busyTimeout ?? 5e3}`);
|
|
956
1128
|
if (!options.readonly && (options.wal ?? true)) {
|
|
957
1129
|
this.raw.exec("PRAGMA journal_mode = WAL");
|
|
958
1130
|
}
|
|
@@ -963,12 +1135,20 @@ var NodeSqliteDriver = class {
|
|
|
963
1135
|
}
|
|
964
1136
|
prepare(sql) {
|
|
965
1137
|
this.verbose?.(sql);
|
|
1138
|
+
const cached = this.cache.get(sql);
|
|
1139
|
+
if (cached) return cached;
|
|
966
1140
|
const stmt = this.raw.prepare(sql);
|
|
967
|
-
|
|
1141
|
+
const wrapped = {
|
|
968
1142
|
run: (...p) => stmt.run(...p),
|
|
969
1143
|
get: (...p) => stmt.get(...p),
|
|
970
1144
|
all: (...p) => stmt.all(...p)
|
|
971
1145
|
};
|
|
1146
|
+
if (this.cache.size >= STMT_CACHE_MAX2) {
|
|
1147
|
+
const oldest = this.cache.keys().next().value;
|
|
1148
|
+
if (oldest !== void 0) this.cache.delete(oldest);
|
|
1149
|
+
}
|
|
1150
|
+
this.cache.set(sql, wrapped);
|
|
1151
|
+
return wrapped;
|
|
972
1152
|
}
|
|
973
1153
|
transaction(fn) {
|
|
974
1154
|
const savepoint = `monlite_sp_${this.depth}`;
|
|
@@ -983,12 +1163,21 @@ var NodeSqliteDriver = class {
|
|
|
983
1163
|
return result;
|
|
984
1164
|
} catch (err) {
|
|
985
1165
|
this.depth--;
|
|
986
|
-
|
|
987
|
-
|
|
1166
|
+
try {
|
|
1167
|
+
if (this.depth === 0) this.raw.exec("ROLLBACK");
|
|
1168
|
+
else this.raw.exec(`ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`);
|
|
1169
|
+
} catch {
|
|
1170
|
+
this.depth = 0;
|
|
1171
|
+
try {
|
|
1172
|
+
this.raw.exec("ROLLBACK");
|
|
1173
|
+
} catch {
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
988
1176
|
throw err;
|
|
989
1177
|
}
|
|
990
1178
|
}
|
|
991
1179
|
close() {
|
|
1180
|
+
this.cache.clear();
|
|
992
1181
|
this.raw.close();
|
|
993
1182
|
}
|
|
994
1183
|
};
|
|
@@ -1041,8 +1230,10 @@ function createDriver(filename, options = {}) {
|
|
|
1041
1230
|
|
|
1042
1231
|
// src/sync/version.ts
|
|
1043
1232
|
var TS_WIDTH = 15;
|
|
1044
|
-
|
|
1045
|
-
|
|
1233
|
+
var SEQ_WIDTH = 12;
|
|
1234
|
+
function makeVersion(ts, nodeId, seq) {
|
|
1235
|
+
const base = String(ts).padStart(TS_WIDTH, "0") + ":" + nodeId;
|
|
1236
|
+
return seq === void 0 ? base : base + ":" + String(seq).padStart(SEQ_WIDTH, "0");
|
|
1046
1237
|
}
|
|
1047
1238
|
function compareVersions(a, b) {
|
|
1048
1239
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -1069,6 +1260,7 @@ var SyncStore = class {
|
|
|
1069
1260
|
}
|
|
1070
1261
|
db;
|
|
1071
1262
|
nodeId;
|
|
1263
|
+
versionSeq = 0;
|
|
1072
1264
|
init() {
|
|
1073
1265
|
this.db.exec(`
|
|
1074
1266
|
CREATE TABLE IF NOT EXISTS _monlite_changes (
|
|
@@ -1117,7 +1309,7 @@ var SyncStore = class {
|
|
|
1117
1309
|
/* ----------------------- local change recording ----------------------- */
|
|
1118
1310
|
/** Append a locally-originated change to the feed. Call inside a write txn. */
|
|
1119
1311
|
recordLocal(collection, id, op, ts) {
|
|
1120
|
-
const version = makeVersion(ts, this.nodeId);
|
|
1312
|
+
const version = makeVersion(ts, this.nodeId, this.versionSeq++);
|
|
1121
1313
|
this.db.prepare(
|
|
1122
1314
|
`INSERT INTO _monlite_changes (coll, doc_id, op, version, ts, source, pushed)
|
|
1123
1315
|
VALUES (?, ?, ?, ?, ?, 'local', 0)`
|
|
@@ -1134,13 +1326,18 @@ var SyncStore = class {
|
|
|
1134
1326
|
}
|
|
1135
1327
|
/* ----------------------------- push side ----------------------------- */
|
|
1136
1328
|
/** Latest unpushed local change per document (the push queue). */
|
|
1137
|
-
pending(collections) {
|
|
1329
|
+
pending(collections, limit) {
|
|
1138
1330
|
const params = [];
|
|
1139
1331
|
let collFilter = "";
|
|
1140
1332
|
if (collections && collections.length) {
|
|
1141
1333
|
collFilter = ` AND coll IN (${collections.map(() => "?").join(", ")})`;
|
|
1142
1334
|
params.push(...collections);
|
|
1143
1335
|
}
|
|
1336
|
+
let limitClause = "";
|
|
1337
|
+
if (limit != null && limit > 0) {
|
|
1338
|
+
limitClause = " LIMIT ?";
|
|
1339
|
+
params.push(limit);
|
|
1340
|
+
}
|
|
1144
1341
|
const rows = this.db.prepare(
|
|
1145
1342
|
`SELECT c.seq, c.coll, c.doc_id, c.op, c.version, c.ts
|
|
1146
1343
|
FROM _monlite_changes c
|
|
@@ -1150,7 +1347,7 @@ var SyncStore = class {
|
|
|
1150
1347
|
WHERE source = 'local' AND pushed = 0${collFilter}
|
|
1151
1348
|
GROUP BY coll, doc_id
|
|
1152
1349
|
) m ON c.coll = m.coll AND c.doc_id = m.doc_id AND c.seq = m.mseq
|
|
1153
|
-
ORDER BY c.seq`
|
|
1350
|
+
ORDER BY c.seq${limitClause}`
|
|
1154
1351
|
).all(...params);
|
|
1155
1352
|
return rows.map((r) => {
|
|
1156
1353
|
const change = {
|
|
@@ -1188,31 +1385,31 @@ var SyncStore = class {
|
|
|
1188
1385
|
*/
|
|
1189
1386
|
applyRemote(change, resolver) {
|
|
1190
1387
|
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
|
-
|
|
1388
|
+
return this.db.transaction(() => {
|
|
1389
|
+
const localVersion = this.currentVersion(change.collection, change._id);
|
|
1390
|
+
let winner;
|
|
1391
|
+
if (localVersion === null) {
|
|
1392
|
+
winner = "remote";
|
|
1393
|
+
} else if (change.version === localVersion) {
|
|
1394
|
+
return { applied: false, conflict: false, winner: "none" };
|
|
1395
|
+
} else {
|
|
1396
|
+
winner = resolver ? resolver({
|
|
1397
|
+
collection: change.collection,
|
|
1398
|
+
_id: change._id,
|
|
1399
|
+
local: { version: localVersion },
|
|
1400
|
+
remote: { version: change.version, doc: change.doc }
|
|
1401
|
+
}) : compareVersions(change.version, localVersion) > 0 ? "remote" : "local";
|
|
1402
|
+
this.recordConflict(
|
|
1403
|
+
change.collection,
|
|
1404
|
+
change._id,
|
|
1405
|
+
localVersion,
|
|
1406
|
+
change.version,
|
|
1407
|
+
winner
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
if (winner !== "remote") {
|
|
1411
|
+
return { applied: false, conflict: localVersion !== null, winner };
|
|
1412
|
+
}
|
|
1216
1413
|
this.applyData(change);
|
|
1217
1414
|
this.db.prepare(
|
|
1218
1415
|
`INSERT INTO _monlite_changes (coll, doc_id, op, version, ts, source, pushed)
|
|
@@ -1224,8 +1421,8 @@ var SyncStore = class {
|
|
|
1224
1421
|
change.version,
|
|
1225
1422
|
versionTs(change.version)
|
|
1226
1423
|
);
|
|
1424
|
+
return { applied: true, conflict: localVersion !== null, winner: "remote" };
|
|
1227
1425
|
});
|
|
1228
|
-
return { applied: true, conflict: localVersion !== null, winner: "remote" };
|
|
1229
1426
|
}
|
|
1230
1427
|
applyData(change) {
|
|
1231
1428
|
const { collection: coll, _id, op } = change;
|
|
@@ -1248,13 +1445,18 @@ var SyncStore = class {
|
|
|
1248
1445
|
* as RemoteChanges (used when this database acts as a sync *source*, e.g. the
|
|
1249
1446
|
* monlite-as-remote adapter). Returns the new watermark to resume from.
|
|
1250
1447
|
*/
|
|
1251
|
-
changesSince(seq, collections) {
|
|
1448
|
+
changesSince(seq, collections, limit) {
|
|
1252
1449
|
const params = [seq];
|
|
1253
1450
|
let collFilter = "";
|
|
1254
1451
|
if (collections && collections.length) {
|
|
1255
1452
|
collFilter = ` AND coll IN (${collections.map(() => "?").join(", ")})`;
|
|
1256
1453
|
params.push(...collections);
|
|
1257
1454
|
}
|
|
1455
|
+
let limitClause = "";
|
|
1456
|
+
if (limit != null && limit > 0) {
|
|
1457
|
+
limitClause = " LIMIT ?";
|
|
1458
|
+
params.push(limit);
|
|
1459
|
+
}
|
|
1258
1460
|
const rows = this.db.prepare(
|
|
1259
1461
|
`SELECT c.seq, c.coll, c.doc_id, c.op, c.version, c.ts
|
|
1260
1462
|
FROM _monlite_changes c
|
|
@@ -1264,7 +1466,7 @@ var SyncStore = class {
|
|
|
1264
1466
|
WHERE seq > ?${collFilter}
|
|
1265
1467
|
GROUP BY coll, doc_id
|
|
1266
1468
|
) m ON c.coll = m.coll AND c.doc_id = m.doc_id AND c.seq = m.mseq
|
|
1267
|
-
ORDER BY c.seq`
|
|
1469
|
+
ORDER BY c.seq${limitClause}`
|
|
1268
1470
|
).all(...params);
|
|
1269
1471
|
const changes = rows.map((r) => {
|
|
1270
1472
|
const change = {
|
|
@@ -1280,8 +1482,8 @@ var SyncStore = class {
|
|
|
1280
1482
|
}
|
|
1281
1483
|
return change;
|
|
1282
1484
|
});
|
|
1283
|
-
const
|
|
1284
|
-
return { changes, maxSeq
|
|
1485
|
+
const maxSeq = rows.length ? rows[rows.length - 1].seq : seq;
|
|
1486
|
+
return { changes, maxSeq };
|
|
1285
1487
|
}
|
|
1286
1488
|
/* ------------------------------ bootstrap ----------------------------- */
|
|
1287
1489
|
/**
|
|
@@ -1414,6 +1616,7 @@ var Monlite = class {
|
|
|
1414
1616
|
driver: options.driver,
|
|
1415
1617
|
readonly: options.readonly,
|
|
1416
1618
|
wal: options.wal,
|
|
1619
|
+
busyTimeout: options.busyTimeout,
|
|
1417
1620
|
verbose: options.verbose
|
|
1418
1621
|
});
|
|
1419
1622
|
this.autoIndexer = new AutoIndexer(
|
|
@@ -1449,6 +1652,15 @@ var Monlite = class {
|
|
|
1449
1652
|
if (!col) {
|
|
1450
1653
|
col = new Collection(this, name, options);
|
|
1451
1654
|
this.collections.set(name, col);
|
|
1655
|
+
} else if (options?.schema) {
|
|
1656
|
+
const requested = Object.keys(options.schema);
|
|
1657
|
+
const existing = new Set(col.columnNames);
|
|
1658
|
+
const sameShape = col.mode === "structured" && requested.length === existing.size && requested.every((c) => existing.has(c));
|
|
1659
|
+
if (!sameShape) {
|
|
1660
|
+
throw new MonliteError(
|
|
1661
|
+
`Collection "${name}" was already opened with a different schema/mode. A collection's storage mode is fixed on first access.`
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1452
1664
|
}
|
|
1453
1665
|
return col;
|
|
1454
1666
|
}
|
|
@@ -1483,7 +1695,11 @@ var Monlite = class {
|
|
|
1483
1695
|
$executeRaw(strings, ...values) {
|
|
1484
1696
|
this.assertOpen();
|
|
1485
1697
|
const { sql, params } = buildTagged(strings, values);
|
|
1486
|
-
|
|
1698
|
+
try {
|
|
1699
|
+
return Promise.resolve(this.driver.prepare(sql).run(...params).changes);
|
|
1700
|
+
} catch (err) {
|
|
1701
|
+
throw normalizeDriverError(err);
|
|
1702
|
+
}
|
|
1487
1703
|
}
|
|
1488
1704
|
/** Like {@link $executeRaw} but takes a raw SQL string and positional params. */
|
|
1489
1705
|
$executeRawUnsafe(sql, ...params) {
|
|
@@ -1543,13 +1759,18 @@ function createDb(filename, options) {
|
|
|
1543
1759
|
|
|
1544
1760
|
exports.Collection = Collection;
|
|
1545
1761
|
exports.Monlite = Monlite;
|
|
1762
|
+
exports.MonliteConstraintError = MonliteConstraintError;
|
|
1546
1763
|
exports.MonliteError = MonliteError;
|
|
1764
|
+
exports.MonliteForeignKeyError = MonliteForeignKeyError;
|
|
1765
|
+
exports.MonliteNotNullError = MonliteNotNullError;
|
|
1547
1766
|
exports.MonliteQueryError = MonliteQueryError;
|
|
1767
|
+
exports.MonliteUniqueConstraintError = MonliteUniqueConstraintError;
|
|
1548
1768
|
exports.SyncStore = SyncStore;
|
|
1549
1769
|
exports.compareVersions = compareVersions;
|
|
1550
1770
|
exports.createDb = createDb;
|
|
1551
1771
|
exports.isObjectId = isObjectId;
|
|
1552
1772
|
exports.makeVersion = makeVersion;
|
|
1773
|
+
exports.normalizeDriverError = normalizeDriverError;
|
|
1553
1774
|
exports.objectId = objectId;
|
|
1554
1775
|
exports.versionTs = versionTs;
|
|
1555
1776
|
//# sourceMappingURL=index.cjs.map
|