@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.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));
|
|
@@ -192,15 +247,17 @@ function inExpr(expr, arr, ctx, negate) {
|
|
|
192
247
|
}).join(", ");
|
|
193
248
|
return negate ? `(${expr} IS NULL OR ${expr} NOT IN (${placeholders}))` : `${expr} IN (${placeholders})`;
|
|
194
249
|
}
|
|
195
|
-
function containsExpr(field, expr, v, ctx) {
|
|
250
|
+
function containsExpr(field, expr, v, ctx, ci) {
|
|
251
|
+
const sub = ci ? `instr(lower(${expr}), lower(?)) > 0` : `instr(${expr}, ?) > 0`;
|
|
196
252
|
if (isColumn(field, ctx.columns)) {
|
|
197
253
|
ctx.params.push(bindable(v));
|
|
198
|
-
return
|
|
254
|
+
return sub;
|
|
199
255
|
}
|
|
200
256
|
const path = pathLiteral(field);
|
|
257
|
+
const member = ci ? `lower(value) = lower(?)` : `value = ?`;
|
|
201
258
|
ctx.params.push(bindable(v));
|
|
202
259
|
ctx.params.push(bindable(v));
|
|
203
|
-
return `(CASE WHEN json_type(data, ${path}) = 'array' THEN EXISTS (SELECT 1 FROM json_each(data, ${path}) WHERE
|
|
260
|
+
return `(CASE WHEN json_type(data, ${path}) = 'array' THEN EXISTS (SELECT 1 FROM json_each(data, ${path}) WHERE ${member}) ELSE ${sub} END)`;
|
|
204
261
|
}
|
|
205
262
|
function hasExpr(field, expr, v, ctx) {
|
|
206
263
|
ctx.params.push(bindable(v));
|
|
@@ -233,16 +290,26 @@ function buildOrderBy(orderBy, onPath, columns) {
|
|
|
233
290
|
}
|
|
234
291
|
|
|
235
292
|
// src/query/path.ts
|
|
293
|
+
var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
294
|
+
function safeSegments(path) {
|
|
295
|
+
const segs = path.split(".");
|
|
296
|
+
for (const seg of segs) {
|
|
297
|
+
if (FORBIDDEN.has(seg)) {
|
|
298
|
+
throw new MonliteQueryError(`Illegal path segment "${seg}" in "${path}"`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return segs;
|
|
302
|
+
}
|
|
236
303
|
function getPath(obj, path) {
|
|
237
304
|
let cur = obj;
|
|
238
|
-
for (const seg of path
|
|
305
|
+
for (const seg of safeSegments(path)) {
|
|
239
306
|
if (cur == null) return void 0;
|
|
240
307
|
cur = cur[seg];
|
|
241
308
|
}
|
|
242
309
|
return cur;
|
|
243
310
|
}
|
|
244
311
|
function setPath(obj, path, value) {
|
|
245
|
-
const segs = path
|
|
312
|
+
const segs = safeSegments(path);
|
|
246
313
|
let cur = obj;
|
|
247
314
|
for (let i = 0; i < segs.length - 1; i++) {
|
|
248
315
|
const seg = segs[i];
|
|
@@ -252,7 +319,7 @@ function setPath(obj, path, value) {
|
|
|
252
319
|
cur[segs[segs.length - 1]] = value;
|
|
253
320
|
}
|
|
254
321
|
function unsetPath(obj, path) {
|
|
255
|
-
const segs = path
|
|
322
|
+
const segs = safeSegments(path);
|
|
256
323
|
let cur = obj;
|
|
257
324
|
for (let i = 0; i < segs.length - 1; i++) {
|
|
258
325
|
const seg = segs[i];
|
|
@@ -305,8 +372,13 @@ function applyUpdate(doc, data) {
|
|
|
305
372
|
}
|
|
306
373
|
if (ops.$inc) {
|
|
307
374
|
for (const [path, by] of Object.entries(ops.$inc)) {
|
|
375
|
+
if (typeof by !== "number" || !Number.isFinite(by)) {
|
|
376
|
+
throw new MonliteQueryError(
|
|
377
|
+
`$inc on "${path}" requires a finite number, got ${JSON.stringify(by)}`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
308
380
|
const cur = getPath(next, path);
|
|
309
|
-
setPath(next, path, (typeof cur === "number" ? cur : 0) +
|
|
381
|
+
setPath(next, path, (typeof cur === "number" ? cur : 0) + by);
|
|
310
382
|
}
|
|
311
383
|
}
|
|
312
384
|
if (ops.$push) {
|
|
@@ -425,7 +497,7 @@ function buildHaving(having, params, columns) {
|
|
|
425
497
|
}
|
|
426
498
|
function groupBy(ctx, args) {
|
|
427
499
|
if (!Array.isArray(args.by) || args.by.length === 0) {
|
|
428
|
-
throw new
|
|
500
|
+
throw new MonliteQueryError("groupBy requires a non-empty `by` array");
|
|
429
501
|
}
|
|
430
502
|
const params = [];
|
|
431
503
|
const where = buildWhere(args.where, {
|
|
@@ -434,13 +506,16 @@ function groupBy(ctx, args) {
|
|
|
434
506
|
columns: ctx.columns
|
|
435
507
|
});
|
|
436
508
|
const groupExprs = [];
|
|
509
|
+
const groupCols = [];
|
|
437
510
|
const selects = [];
|
|
438
|
-
|
|
511
|
+
args.by.forEach((field, gi) => {
|
|
439
512
|
if (!isColumn(field, ctx.columns)) ctx.onPath(field);
|
|
440
513
|
const expr = fieldExpr(field, ctx.columns);
|
|
514
|
+
const alias = `grp_${gi}`;
|
|
441
515
|
groupExprs.push(expr);
|
|
442
|
-
selects.push(`${expr} AS
|
|
443
|
-
|
|
516
|
+
selects.push(`${expr} AS ${alias}`);
|
|
517
|
+
groupCols.push({ alias, field });
|
|
518
|
+
});
|
|
444
519
|
selects.push(`COUNT(*) AS agg_count`);
|
|
445
520
|
const { selects: accSelects, cols } = buildAccumulators(args, ctx.onPath, ctx.columns);
|
|
446
521
|
selects.push(...accSelects);
|
|
@@ -469,7 +544,7 @@ function groupBy(ctx, args) {
|
|
|
469
544
|
const rows = ctx.db.prepare(sql).all(...params);
|
|
470
545
|
return rows.map((row) => {
|
|
471
546
|
const out = {};
|
|
472
|
-
for (const field of
|
|
547
|
+
for (const { alias, field } of groupCols) out[field] = row[alias];
|
|
473
548
|
if (args._count) out._count = row.agg_count;
|
|
474
549
|
for (const col of cols) {
|
|
475
550
|
(out[col.kind] ??= {})[col.field] = row[col.alias] ?? null;
|
|
@@ -508,6 +583,13 @@ var Collection = class {
|
|
|
508
583
|
);
|
|
509
584
|
}
|
|
510
585
|
const normalized = typeof def === "string" ? { type: def } : def;
|
|
586
|
+
if (normalized.references && !/^[A-Za-z_][A-Za-z0-9_]*(\([A-Za-z_][A-Za-z0-9_]*\))?$/.test(
|
|
587
|
+
normalized.references
|
|
588
|
+
)) {
|
|
589
|
+
throw new MonliteError(
|
|
590
|
+
`Invalid references "${normalized.references}" on column "${field}"`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
511
593
|
this.columnDefs[field] = normalized;
|
|
512
594
|
this.columnOrder.push(field);
|
|
513
595
|
this.columns.add(field);
|
|
@@ -529,6 +611,14 @@ var Collection = class {
|
|
|
529
611
|
get db() {
|
|
530
612
|
return this.mon.driver;
|
|
531
613
|
}
|
|
614
|
+
/** Run a DB operation, normalizing driver errors into typed MonliteErrors. */
|
|
615
|
+
guard(fn) {
|
|
616
|
+
try {
|
|
617
|
+
return fn();
|
|
618
|
+
} catch (err) {
|
|
619
|
+
throw normalizeDriverError(err, this.name);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
532
622
|
/** Native column names declared for this collection (structured mode). */
|
|
533
623
|
get columnNames() {
|
|
534
624
|
return [...this.columnOrder];
|
|
@@ -581,7 +671,11 @@ var Collection = class {
|
|
|
581
671
|
if (this.mode === "structured") {
|
|
582
672
|
for (const field of this.columnOrder) {
|
|
583
673
|
const value = row[field];
|
|
584
|
-
if (value ===
|
|
674
|
+
if (value === void 0) continue;
|
|
675
|
+
if (value === null) {
|
|
676
|
+
doc[field] = null;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
585
679
|
doc[field] = this.jsonColumns.has(field) ? JSON.parse(value) : value;
|
|
586
680
|
}
|
|
587
681
|
}
|
|
@@ -594,6 +688,11 @@ var Collection = class {
|
|
|
594
688
|
if (this.jsonColumns.has(field)) {
|
|
595
689
|
return value === void 0 ? null : JSON.stringify(value);
|
|
596
690
|
}
|
|
691
|
+
if (value !== null && typeof value === "object" && !(value instanceof Date) && !Buffer.isBuffer(value)) {
|
|
692
|
+
throw new MonliteQueryError(
|
|
693
|
+
`Column "${field}" cannot store an object/array. Declare it as { type: "JSON" } to store structured values.`
|
|
694
|
+
);
|
|
695
|
+
}
|
|
597
696
|
return bindable(value);
|
|
598
697
|
}
|
|
599
698
|
insertColumns() {
|
|
@@ -676,21 +775,22 @@ var Collection = class {
|
|
|
676
775
|
this.db.prepare(this.insertSql()).run(...row.values);
|
|
677
776
|
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
678
777
|
};
|
|
679
|
-
|
|
680
|
-
else write();
|
|
778
|
+
this.guard(() => recorder ? this.db.transaction(write) : write());
|
|
681
779
|
return row.returned;
|
|
682
780
|
}
|
|
683
781
|
async createMany(args) {
|
|
684
782
|
this.ensureTable();
|
|
685
783
|
const stmt = this.db.prepare(this.insertSql());
|
|
686
784
|
const recorder = this.recorder;
|
|
687
|
-
this.
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
785
|
+
this.guard(
|
|
786
|
+
() => this.db.transaction(() => {
|
|
787
|
+
for (const item of args.data) {
|
|
788
|
+
const row = this.buildInsert(item);
|
|
789
|
+
stmt.run(...row.values);
|
|
790
|
+
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
791
|
+
}
|
|
792
|
+
})
|
|
793
|
+
);
|
|
694
794
|
return { count: args.data.length };
|
|
695
795
|
}
|
|
696
796
|
/* ------------------------------ read ------------------------------ */
|
|
@@ -720,6 +820,28 @@ var Collection = class {
|
|
|
720
820
|
const rows = await this.findMany({ ...args, take: 1 });
|
|
721
821
|
return rows[0] ?? null;
|
|
722
822
|
}
|
|
823
|
+
/** Alias of {@link findFirst} for Prisma familiarity. */
|
|
824
|
+
async findUnique(args = {}) {
|
|
825
|
+
return this.findFirst(args);
|
|
826
|
+
}
|
|
827
|
+
/** Like {@link findFirst} but throws if no document matches. */
|
|
828
|
+
async findFirstOrThrow(args = {}) {
|
|
829
|
+
const doc = await this.findFirst(args);
|
|
830
|
+
if (!doc) throw new MonliteError(`No document found in "${this.name}"`);
|
|
831
|
+
return doc;
|
|
832
|
+
}
|
|
833
|
+
/** True if at least one document matches. */
|
|
834
|
+
async exists(where) {
|
|
835
|
+
this.ensureTable();
|
|
836
|
+
const params = [];
|
|
837
|
+
const clause = buildWhere(where, {
|
|
838
|
+
params,
|
|
839
|
+
onPath: this.trackPath,
|
|
840
|
+
columns: this.columns
|
|
841
|
+
});
|
|
842
|
+
const row = this.db.prepare(`SELECT 1 FROM "${this.name}" WHERE ${clause} LIMIT 1`).get(...params);
|
|
843
|
+
return row != null;
|
|
844
|
+
}
|
|
723
845
|
async findById(id) {
|
|
724
846
|
this.ensureTable();
|
|
725
847
|
const row = this.db.prepare(`SELECT * FROM "${this.name}" WHERE _id = ?`).get(id);
|
|
@@ -755,8 +877,10 @@ var Collection = class {
|
|
|
755
877
|
this.trackPath(field);
|
|
756
878
|
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
879
|
}
|
|
758
|
-
|
|
759
|
-
|
|
880
|
+
return this.guard(() => {
|
|
881
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
882
|
+
return rows.map((r) => r.v);
|
|
883
|
+
});
|
|
760
884
|
}
|
|
761
885
|
/* ----------------------------- update ----------------------------- */
|
|
762
886
|
runUpdate(where, data, single) {
|
|
@@ -773,23 +897,25 @@ var Collection = class {
|
|
|
773
897
|
if (!rows.length) return [];
|
|
774
898
|
const now = Date.now();
|
|
775
899
|
const recorder = this.recorder;
|
|
776
|
-
return this.
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
900
|
+
return this.guard(
|
|
901
|
+
() => this.db.transaction(() => {
|
|
902
|
+
const out = [];
|
|
903
|
+
for (const row of rows) {
|
|
904
|
+
const current = stripSystem(this.rowToDoc(row));
|
|
905
|
+
const updated = stripSystem(applyUpdate(current, data));
|
|
906
|
+
const { setSql, values } = this.buildUpdateSet(updated, now);
|
|
907
|
+
this.db.prepare(`UPDATE "${this.name}" SET ${setSql} WHERE _id = ?`).run(...values, row._id);
|
|
908
|
+
recorder?.recordLocal(this.name, row._id, "upsert", now);
|
|
909
|
+
out.push({
|
|
910
|
+
...updated,
|
|
911
|
+
_id: row._id,
|
|
912
|
+
created_at: row.created_at,
|
|
913
|
+
updated_at: now
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
return out;
|
|
917
|
+
})
|
|
918
|
+
);
|
|
793
919
|
}
|
|
794
920
|
async update(args) {
|
|
795
921
|
return this.runUpdate(args.where, args.data, true)[0] ?? null;
|
|
@@ -799,15 +925,36 @@ var Collection = class {
|
|
|
799
925
|
}
|
|
800
926
|
async upsert(args) {
|
|
801
927
|
this.ensureTable();
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
928
|
+
return this.guard(
|
|
929
|
+
() => this.db.transaction(() => {
|
|
930
|
+
const params = [];
|
|
931
|
+
const clause = buildWhere(args.where, {
|
|
932
|
+
params,
|
|
933
|
+
onPath: this.trackPath,
|
|
934
|
+
columns: this.columns
|
|
935
|
+
});
|
|
936
|
+
const row = this.db.prepare(`SELECT * FROM "${this.name}" WHERE ${clause} LIMIT 1`).get(...params);
|
|
937
|
+
const now = Date.now();
|
|
938
|
+
const recorder = this.recorder;
|
|
939
|
+
if (row) {
|
|
940
|
+
const current = stripSystem(this.rowToDoc(row));
|
|
941
|
+
const updated = stripSystem(applyUpdate(current, args.update));
|
|
942
|
+
const { setSql, values } = this.buildUpdateSet(updated, now);
|
|
943
|
+
this.db.prepare(`UPDATE "${this.name}" SET ${setSql} WHERE _id = ?`).run(...values, row._id);
|
|
944
|
+
recorder?.recordLocal(this.name, row._id, "upsert", now);
|
|
945
|
+
return {
|
|
946
|
+
...updated,
|
|
947
|
+
_id: row._id,
|
|
948
|
+
created_at: row.created_at,
|
|
949
|
+
updated_at: now
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const ins = this.buildInsert(args.create);
|
|
953
|
+
this.db.prepare(this.insertSql()).run(...ins.values);
|
|
954
|
+
recorder?.recordLocal(this.name, ins._id, "upsert", ins.created_at);
|
|
955
|
+
return ins.returned;
|
|
956
|
+
})
|
|
957
|
+
);
|
|
811
958
|
}
|
|
812
959
|
/* ----------------------------- delete ----------------------------- */
|
|
813
960
|
runDelete(where, single) {
|
|
@@ -825,12 +972,14 @@ var Collection = class {
|
|
|
825
972
|
const stmt = this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`);
|
|
826
973
|
const recorder = this.recorder;
|
|
827
974
|
const now = Date.now();
|
|
828
|
-
this.
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
975
|
+
this.guard(
|
|
976
|
+
() => this.db.transaction(() => {
|
|
977
|
+
for (const row of rows) {
|
|
978
|
+
stmt.run(row._id);
|
|
979
|
+
recorder?.recordLocal(this.name, row._id, "delete", now);
|
|
980
|
+
}
|
|
981
|
+
})
|
|
982
|
+
);
|
|
834
983
|
return rows.map((r) => this.rowToDoc(r));
|
|
835
984
|
}
|
|
836
985
|
async delete(args) {
|
|
@@ -842,16 +991,20 @@ var Collection = class {
|
|
|
842
991
|
/* --------------------------- aggregation -------------------------- */
|
|
843
992
|
async aggregate(args = {}) {
|
|
844
993
|
this.ensureTable();
|
|
845
|
-
return
|
|
846
|
-
|
|
847
|
-
|
|
994
|
+
return this.guard(
|
|
995
|
+
() => aggregate(
|
|
996
|
+
{ db: this.db, table: this.name, onPath: this.trackPath, columns: this.columns },
|
|
997
|
+
args
|
|
998
|
+
)
|
|
848
999
|
);
|
|
849
1000
|
}
|
|
850
1001
|
async groupBy(args) {
|
|
851
1002
|
this.ensureTable();
|
|
852
|
-
return
|
|
853
|
-
|
|
854
|
-
|
|
1003
|
+
return this.guard(
|
|
1004
|
+
() => groupBy(
|
|
1005
|
+
{ db: this.db, table: this.name, onPath: this.trackPath, columns: this.columns },
|
|
1006
|
+
args
|
|
1007
|
+
)
|
|
855
1008
|
);
|
|
856
1009
|
}
|
|
857
1010
|
};
|
|
@@ -909,15 +1062,19 @@ var AutoIndexer = class {
|
|
|
909
1062
|
};
|
|
910
1063
|
|
|
911
1064
|
// src/driver/better-sqlite3.ts
|
|
1065
|
+
var STMT_CACHE_MAX = 256;
|
|
912
1066
|
var BetterSqlite3Driver = class {
|
|
913
1067
|
name = "better-sqlite3";
|
|
914
1068
|
raw;
|
|
915
1069
|
verbose;
|
|
1070
|
+
cache = /* @__PURE__ */ new Map();
|
|
916
1071
|
constructor(BetterSqlite3, filename, options) {
|
|
917
1072
|
this.verbose = options.verbose;
|
|
918
1073
|
this.raw = new BetterSqlite3(filename, {
|
|
919
1074
|
readonly: options.readonly ?? false
|
|
920
1075
|
});
|
|
1076
|
+
this.raw.pragma("foreign_keys = ON");
|
|
1077
|
+
this.raw.pragma(`busy_timeout = ${options.busyTimeout ?? 5e3}`);
|
|
921
1078
|
if (!options.readonly && (options.wal ?? true)) {
|
|
922
1079
|
this.raw.pragma("journal_mode = WAL");
|
|
923
1080
|
}
|
|
@@ -928,21 +1085,35 @@ var BetterSqlite3Driver = class {
|
|
|
928
1085
|
}
|
|
929
1086
|
prepare(sql) {
|
|
930
1087
|
this.verbose?.(sql);
|
|
931
|
-
|
|
1088
|
+
const cached = this.cache.get(sql);
|
|
1089
|
+
if (cached) return cached;
|
|
1090
|
+
const stmt = this.raw.prepare(sql);
|
|
1091
|
+
this.cacheStmt(sql, stmt);
|
|
1092
|
+
return stmt;
|
|
1093
|
+
}
|
|
1094
|
+
cacheStmt(sql, stmt) {
|
|
1095
|
+
if (this.cache.size >= STMT_CACHE_MAX) {
|
|
1096
|
+
const oldest = this.cache.keys().next().value;
|
|
1097
|
+
if (oldest !== void 0) this.cache.delete(oldest);
|
|
1098
|
+
}
|
|
1099
|
+
this.cache.set(sql, stmt);
|
|
932
1100
|
}
|
|
933
1101
|
transaction(fn) {
|
|
934
1102
|
return this.raw.transaction(fn)();
|
|
935
1103
|
}
|
|
936
1104
|
close() {
|
|
1105
|
+
this.cache.clear();
|
|
937
1106
|
this.raw.close();
|
|
938
1107
|
}
|
|
939
1108
|
};
|
|
940
1109
|
|
|
941
1110
|
// src/driver/node-sqlite.ts
|
|
1111
|
+
var STMT_CACHE_MAX2 = 256;
|
|
942
1112
|
var NodeSqliteDriver = class {
|
|
943
1113
|
name = "node:sqlite";
|
|
944
1114
|
raw;
|
|
945
1115
|
verbose;
|
|
1116
|
+
cache = /* @__PURE__ */ new Map();
|
|
946
1117
|
depth = 0;
|
|
947
1118
|
constructor(nodeSqlite, filename, options) {
|
|
948
1119
|
this.verbose = options.verbose;
|
|
@@ -950,6 +1121,7 @@ var NodeSqliteDriver = class {
|
|
|
950
1121
|
this.raw = new DatabaseSync(filename, {
|
|
951
1122
|
readOnly: options.readonly ?? false
|
|
952
1123
|
});
|
|
1124
|
+
this.raw.exec(`PRAGMA busy_timeout = ${options.busyTimeout ?? 5e3}`);
|
|
953
1125
|
if (!options.readonly && (options.wal ?? true)) {
|
|
954
1126
|
this.raw.exec("PRAGMA journal_mode = WAL");
|
|
955
1127
|
}
|
|
@@ -960,12 +1132,20 @@ var NodeSqliteDriver = class {
|
|
|
960
1132
|
}
|
|
961
1133
|
prepare(sql) {
|
|
962
1134
|
this.verbose?.(sql);
|
|
1135
|
+
const cached = this.cache.get(sql);
|
|
1136
|
+
if (cached) return cached;
|
|
963
1137
|
const stmt = this.raw.prepare(sql);
|
|
964
|
-
|
|
1138
|
+
const wrapped = {
|
|
965
1139
|
run: (...p) => stmt.run(...p),
|
|
966
1140
|
get: (...p) => stmt.get(...p),
|
|
967
1141
|
all: (...p) => stmt.all(...p)
|
|
968
1142
|
};
|
|
1143
|
+
if (this.cache.size >= STMT_CACHE_MAX2) {
|
|
1144
|
+
const oldest = this.cache.keys().next().value;
|
|
1145
|
+
if (oldest !== void 0) this.cache.delete(oldest);
|
|
1146
|
+
}
|
|
1147
|
+
this.cache.set(sql, wrapped);
|
|
1148
|
+
return wrapped;
|
|
969
1149
|
}
|
|
970
1150
|
transaction(fn) {
|
|
971
1151
|
const savepoint = `monlite_sp_${this.depth}`;
|
|
@@ -980,12 +1160,21 @@ var NodeSqliteDriver = class {
|
|
|
980
1160
|
return result;
|
|
981
1161
|
} catch (err) {
|
|
982
1162
|
this.depth--;
|
|
983
|
-
|
|
984
|
-
|
|
1163
|
+
try {
|
|
1164
|
+
if (this.depth === 0) this.raw.exec("ROLLBACK");
|
|
1165
|
+
else this.raw.exec(`ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`);
|
|
1166
|
+
} catch {
|
|
1167
|
+
this.depth = 0;
|
|
1168
|
+
try {
|
|
1169
|
+
this.raw.exec("ROLLBACK");
|
|
1170
|
+
} catch {
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
985
1173
|
throw err;
|
|
986
1174
|
}
|
|
987
1175
|
}
|
|
988
1176
|
close() {
|
|
1177
|
+
this.cache.clear();
|
|
989
1178
|
this.raw.close();
|
|
990
1179
|
}
|
|
991
1180
|
};
|
|
@@ -1038,8 +1227,10 @@ function createDriver(filename, options = {}) {
|
|
|
1038
1227
|
|
|
1039
1228
|
// src/sync/version.ts
|
|
1040
1229
|
var TS_WIDTH = 15;
|
|
1041
|
-
|
|
1042
|
-
|
|
1230
|
+
var SEQ_WIDTH = 12;
|
|
1231
|
+
function makeVersion(ts, nodeId, seq) {
|
|
1232
|
+
const base = String(ts).padStart(TS_WIDTH, "0") + ":" + nodeId;
|
|
1233
|
+
return seq === void 0 ? base : base + ":" + String(seq).padStart(SEQ_WIDTH, "0");
|
|
1043
1234
|
}
|
|
1044
1235
|
function compareVersions(a, b) {
|
|
1045
1236
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -1066,6 +1257,7 @@ var SyncStore = class {
|
|
|
1066
1257
|
}
|
|
1067
1258
|
db;
|
|
1068
1259
|
nodeId;
|
|
1260
|
+
versionSeq = 0;
|
|
1069
1261
|
init() {
|
|
1070
1262
|
this.db.exec(`
|
|
1071
1263
|
CREATE TABLE IF NOT EXISTS _monlite_changes (
|
|
@@ -1114,7 +1306,7 @@ var SyncStore = class {
|
|
|
1114
1306
|
/* ----------------------- local change recording ----------------------- */
|
|
1115
1307
|
/** Append a locally-originated change to the feed. Call inside a write txn. */
|
|
1116
1308
|
recordLocal(collection, id, op, ts) {
|
|
1117
|
-
const version = makeVersion(ts, this.nodeId);
|
|
1309
|
+
const version = makeVersion(ts, this.nodeId, this.versionSeq++);
|
|
1118
1310
|
this.db.prepare(
|
|
1119
1311
|
`INSERT INTO _monlite_changes (coll, doc_id, op, version, ts, source, pushed)
|
|
1120
1312
|
VALUES (?, ?, ?, ?, ?, 'local', 0)`
|
|
@@ -1131,13 +1323,18 @@ var SyncStore = class {
|
|
|
1131
1323
|
}
|
|
1132
1324
|
/* ----------------------------- push side ----------------------------- */
|
|
1133
1325
|
/** Latest unpushed local change per document (the push queue). */
|
|
1134
|
-
pending(collections) {
|
|
1326
|
+
pending(collections, limit) {
|
|
1135
1327
|
const params = [];
|
|
1136
1328
|
let collFilter = "";
|
|
1137
1329
|
if (collections && collections.length) {
|
|
1138
1330
|
collFilter = ` AND coll IN (${collections.map(() => "?").join(", ")})`;
|
|
1139
1331
|
params.push(...collections);
|
|
1140
1332
|
}
|
|
1333
|
+
let limitClause = "";
|
|
1334
|
+
if (limit != null && limit > 0) {
|
|
1335
|
+
limitClause = " LIMIT ?";
|
|
1336
|
+
params.push(limit);
|
|
1337
|
+
}
|
|
1141
1338
|
const rows = this.db.prepare(
|
|
1142
1339
|
`SELECT c.seq, c.coll, c.doc_id, c.op, c.version, c.ts
|
|
1143
1340
|
FROM _monlite_changes c
|
|
@@ -1147,7 +1344,7 @@ var SyncStore = class {
|
|
|
1147
1344
|
WHERE source = 'local' AND pushed = 0${collFilter}
|
|
1148
1345
|
GROUP BY coll, doc_id
|
|
1149
1346
|
) m ON c.coll = m.coll AND c.doc_id = m.doc_id AND c.seq = m.mseq
|
|
1150
|
-
ORDER BY c.seq`
|
|
1347
|
+
ORDER BY c.seq${limitClause}`
|
|
1151
1348
|
).all(...params);
|
|
1152
1349
|
return rows.map((r) => {
|
|
1153
1350
|
const change = {
|
|
@@ -1185,31 +1382,31 @@ var SyncStore = class {
|
|
|
1185
1382
|
*/
|
|
1186
1383
|
applyRemote(change, resolver) {
|
|
1187
1384
|
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
|
-
|
|
1385
|
+
return this.db.transaction(() => {
|
|
1386
|
+
const localVersion = this.currentVersion(change.collection, change._id);
|
|
1387
|
+
let winner;
|
|
1388
|
+
if (localVersion === null) {
|
|
1389
|
+
winner = "remote";
|
|
1390
|
+
} else if (change.version === localVersion) {
|
|
1391
|
+
return { applied: false, conflict: false, winner: "none" };
|
|
1392
|
+
} else {
|
|
1393
|
+
winner = resolver ? resolver({
|
|
1394
|
+
collection: change.collection,
|
|
1395
|
+
_id: change._id,
|
|
1396
|
+
local: { version: localVersion },
|
|
1397
|
+
remote: { version: change.version, doc: change.doc }
|
|
1398
|
+
}) : compareVersions(change.version, localVersion) > 0 ? "remote" : "local";
|
|
1399
|
+
this.recordConflict(
|
|
1400
|
+
change.collection,
|
|
1401
|
+
change._id,
|
|
1402
|
+
localVersion,
|
|
1403
|
+
change.version,
|
|
1404
|
+
winner
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
if (winner !== "remote") {
|
|
1408
|
+
return { applied: false, conflict: localVersion !== null, winner };
|
|
1409
|
+
}
|
|
1213
1410
|
this.applyData(change);
|
|
1214
1411
|
this.db.prepare(
|
|
1215
1412
|
`INSERT INTO _monlite_changes (coll, doc_id, op, version, ts, source, pushed)
|
|
@@ -1221,8 +1418,8 @@ var SyncStore = class {
|
|
|
1221
1418
|
change.version,
|
|
1222
1419
|
versionTs(change.version)
|
|
1223
1420
|
);
|
|
1421
|
+
return { applied: true, conflict: localVersion !== null, winner: "remote" };
|
|
1224
1422
|
});
|
|
1225
|
-
return { applied: true, conflict: localVersion !== null, winner: "remote" };
|
|
1226
1423
|
}
|
|
1227
1424
|
applyData(change) {
|
|
1228
1425
|
const { collection: coll, _id, op } = change;
|
|
@@ -1245,13 +1442,18 @@ var SyncStore = class {
|
|
|
1245
1442
|
* as RemoteChanges (used when this database acts as a sync *source*, e.g. the
|
|
1246
1443
|
* monlite-as-remote adapter). Returns the new watermark to resume from.
|
|
1247
1444
|
*/
|
|
1248
|
-
changesSince(seq, collections) {
|
|
1445
|
+
changesSince(seq, collections, limit) {
|
|
1249
1446
|
const params = [seq];
|
|
1250
1447
|
let collFilter = "";
|
|
1251
1448
|
if (collections && collections.length) {
|
|
1252
1449
|
collFilter = ` AND coll IN (${collections.map(() => "?").join(", ")})`;
|
|
1253
1450
|
params.push(...collections);
|
|
1254
1451
|
}
|
|
1452
|
+
let limitClause = "";
|
|
1453
|
+
if (limit != null && limit > 0) {
|
|
1454
|
+
limitClause = " LIMIT ?";
|
|
1455
|
+
params.push(limit);
|
|
1456
|
+
}
|
|
1255
1457
|
const rows = this.db.prepare(
|
|
1256
1458
|
`SELECT c.seq, c.coll, c.doc_id, c.op, c.version, c.ts
|
|
1257
1459
|
FROM _monlite_changes c
|
|
@@ -1261,7 +1463,7 @@ var SyncStore = class {
|
|
|
1261
1463
|
WHERE seq > ?${collFilter}
|
|
1262
1464
|
GROUP BY coll, doc_id
|
|
1263
1465
|
) m ON c.coll = m.coll AND c.doc_id = m.doc_id AND c.seq = m.mseq
|
|
1264
|
-
ORDER BY c.seq`
|
|
1466
|
+
ORDER BY c.seq${limitClause}`
|
|
1265
1467
|
).all(...params);
|
|
1266
1468
|
const changes = rows.map((r) => {
|
|
1267
1469
|
const change = {
|
|
@@ -1277,8 +1479,8 @@ var SyncStore = class {
|
|
|
1277
1479
|
}
|
|
1278
1480
|
return change;
|
|
1279
1481
|
});
|
|
1280
|
-
const
|
|
1281
|
-
return { changes, maxSeq
|
|
1482
|
+
const maxSeq = rows.length ? rows[rows.length - 1].seq : seq;
|
|
1483
|
+
return { changes, maxSeq };
|
|
1282
1484
|
}
|
|
1283
1485
|
/* ------------------------------ bootstrap ----------------------------- */
|
|
1284
1486
|
/**
|
|
@@ -1411,6 +1613,7 @@ var Monlite = class {
|
|
|
1411
1613
|
driver: options.driver,
|
|
1412
1614
|
readonly: options.readonly,
|
|
1413
1615
|
wal: options.wal,
|
|
1616
|
+
busyTimeout: options.busyTimeout,
|
|
1414
1617
|
verbose: options.verbose
|
|
1415
1618
|
});
|
|
1416
1619
|
this.autoIndexer = new AutoIndexer(
|
|
@@ -1446,6 +1649,15 @@ var Monlite = class {
|
|
|
1446
1649
|
if (!col) {
|
|
1447
1650
|
col = new Collection(this, name, options);
|
|
1448
1651
|
this.collections.set(name, col);
|
|
1652
|
+
} else if (options?.schema) {
|
|
1653
|
+
const requested = Object.keys(options.schema);
|
|
1654
|
+
const existing = new Set(col.columnNames);
|
|
1655
|
+
const sameShape = col.mode === "structured" && requested.length === existing.size && requested.every((c) => existing.has(c));
|
|
1656
|
+
if (!sameShape) {
|
|
1657
|
+
throw new MonliteError(
|
|
1658
|
+
`Collection "${name}" was already opened with a different schema/mode. A collection's storage mode is fixed on first access.`
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1449
1661
|
}
|
|
1450
1662
|
return col;
|
|
1451
1663
|
}
|
|
@@ -1480,7 +1692,11 @@ var Monlite = class {
|
|
|
1480
1692
|
$executeRaw(strings, ...values) {
|
|
1481
1693
|
this.assertOpen();
|
|
1482
1694
|
const { sql, params } = buildTagged(strings, values);
|
|
1483
|
-
|
|
1695
|
+
try {
|
|
1696
|
+
return Promise.resolve(this.driver.prepare(sql).run(...params).changes);
|
|
1697
|
+
} catch (err) {
|
|
1698
|
+
throw normalizeDriverError(err);
|
|
1699
|
+
}
|
|
1484
1700
|
}
|
|
1485
1701
|
/** Like {@link $executeRaw} but takes a raw SQL string and positional params. */
|
|
1486
1702
|
$executeRawUnsafe(sql, ...params) {
|
|
@@ -1538,6 +1754,6 @@ function createDb(filename, options) {
|
|
|
1538
1754
|
return new Monlite(filename, options);
|
|
1539
1755
|
}
|
|
1540
1756
|
|
|
1541
|
-
export { Collection, Monlite, MonliteError, MonliteQueryError, SyncStore, compareVersions, createDb, isObjectId, makeVersion, objectId, versionTs };
|
|
1757
|
+
export { Collection, Monlite, MonliteConstraintError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, MonliteQueryError, MonliteUniqueConstraintError, SyncStore, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
|
|
1542
1758
|
//# sourceMappingURL=index.js.map
|
|
1543
1759
|
//# sourceMappingURL=index.js.map
|