@monlite/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +343 -0
- package/dist/index.cjs +814 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +271 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +802 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var Database = require('better-sqlite3');
|
|
4
|
+
var crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
9
|
+
|
|
10
|
+
// src/db.ts
|
|
11
|
+
var PROCESS_UNIQUE = crypto.randomBytes(5);
|
|
12
|
+
var counter = crypto.randomBytes(3).readUIntBE(0, 3);
|
|
13
|
+
function objectId() {
|
|
14
|
+
const time = Math.floor(Date.now() / 1e3);
|
|
15
|
+
counter = (counter + 1) % 16777216;
|
|
16
|
+
const buf = Buffer.allocUnsafe(12);
|
|
17
|
+
buf.writeUInt32BE(time >>> 0, 0);
|
|
18
|
+
PROCESS_UNIQUE.copy(buf, 4, 0, 5);
|
|
19
|
+
buf.writeUIntBE(counter, 9, 3);
|
|
20
|
+
return buf.toString("hex");
|
|
21
|
+
}
|
|
22
|
+
function isObjectId(value) {
|
|
23
|
+
return typeof value === "string" && /^[0-9a-f]{24}$/i.test(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/errors.ts
|
|
27
|
+
var MonliteError = class extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "MonliteError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var MonliteQueryError = class extends MonliteError {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "MonliteQueryError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/query/sql.ts
|
|
41
|
+
var RESERVED_FIELDS = /* @__PURE__ */ new Set(["_id", "created_at", "updated_at"]);
|
|
42
|
+
function isReserved(field) {
|
|
43
|
+
return RESERVED_FIELDS.has(field);
|
|
44
|
+
}
|
|
45
|
+
function jsonPath(field) {
|
|
46
|
+
let path = "$";
|
|
47
|
+
for (const seg of field.split(".")) {
|
|
48
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(seg)) {
|
|
49
|
+
path += "." + seg;
|
|
50
|
+
} else if (/^\d+$/.test(seg)) {
|
|
51
|
+
path += "[" + seg + "]";
|
|
52
|
+
} else {
|
|
53
|
+
path += '."' + seg.replace(/"/g, '""') + '"';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return path;
|
|
57
|
+
}
|
|
58
|
+
function pathLiteral(field) {
|
|
59
|
+
return "'" + jsonPath(field).replace(/'/g, "''") + "'";
|
|
60
|
+
}
|
|
61
|
+
function fieldExpr(field) {
|
|
62
|
+
if (isReserved(field)) return `"${field}"`;
|
|
63
|
+
return `json_extract(data, ${pathLiteral(field)})`;
|
|
64
|
+
}
|
|
65
|
+
function bindable(value) {
|
|
66
|
+
if (value === void 0 || value === null) return null;
|
|
67
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
68
|
+
if (value instanceof Date) return value.toISOString();
|
|
69
|
+
if (typeof value === "number" || typeof value === "string" || typeof value === "bigint") {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
if (Buffer.isBuffer(value)) return value;
|
|
73
|
+
return JSON.stringify(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/query/where.ts
|
|
77
|
+
function buildWhere(where, ctx) {
|
|
78
|
+
if (!where) return "1";
|
|
79
|
+
return translateObject(where, ctx) || "1";
|
|
80
|
+
}
|
|
81
|
+
function asArray(v) {
|
|
82
|
+
return Array.isArray(v) ? v : [v];
|
|
83
|
+
}
|
|
84
|
+
function translateObject(where, ctx) {
|
|
85
|
+
const parts = [];
|
|
86
|
+
for (const key of Object.keys(where)) {
|
|
87
|
+
const value = where[key];
|
|
88
|
+
if (value === void 0) continue;
|
|
89
|
+
if (key === "AND" || key === "OR") {
|
|
90
|
+
const subs = asArray(value).map((w) => translateObject(w, ctx)).filter(Boolean);
|
|
91
|
+
if (subs.length) {
|
|
92
|
+
const join = key === "AND" ? " AND " : " OR ";
|
|
93
|
+
parts.push("(" + subs.join(join) + ")");
|
|
94
|
+
}
|
|
95
|
+
} else if (key === "NOT") {
|
|
96
|
+
const subs = asArray(value).map((w) => translateObject(w, ctx)).filter(Boolean);
|
|
97
|
+
if (subs.length) parts.push("NOT (" + subs.join(" AND ") + ")");
|
|
98
|
+
} else {
|
|
99
|
+
const clause = translateField(key, value, ctx);
|
|
100
|
+
if (clause) parts.push(clause);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return parts.join(" AND ");
|
|
104
|
+
}
|
|
105
|
+
function isFilterObject(v) {
|
|
106
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date) && !Buffer.isBuffer(v) && (v.constructor === Object || v.constructor === void 0);
|
|
107
|
+
}
|
|
108
|
+
function translateField(field, condition, ctx) {
|
|
109
|
+
if (ctx.onPath && !isReserved(field)) ctx.onPath(field);
|
|
110
|
+
const expr = fieldExpr(field);
|
|
111
|
+
if (!isFilterObject(condition)) {
|
|
112
|
+
return eqExpr(expr, condition, ctx);
|
|
113
|
+
}
|
|
114
|
+
const filter = condition;
|
|
115
|
+
const clauses = [];
|
|
116
|
+
for (const op of Object.keys(filter)) {
|
|
117
|
+
const v = filter[op];
|
|
118
|
+
if (v === void 0) continue;
|
|
119
|
+
switch (op) {
|
|
120
|
+
case "equals":
|
|
121
|
+
clauses.push(eqExpr(expr, v, ctx));
|
|
122
|
+
break;
|
|
123
|
+
case "not":
|
|
124
|
+
clauses.push(notExpr(expr, v, ctx));
|
|
125
|
+
break;
|
|
126
|
+
case "gt":
|
|
127
|
+
clauses.push(cmp(expr, ">", v, ctx));
|
|
128
|
+
break;
|
|
129
|
+
case "gte":
|
|
130
|
+
clauses.push(cmp(expr, ">=", v, ctx));
|
|
131
|
+
break;
|
|
132
|
+
case "lt":
|
|
133
|
+
clauses.push(cmp(expr, "<", v, ctx));
|
|
134
|
+
break;
|
|
135
|
+
case "lte":
|
|
136
|
+
clauses.push(cmp(expr, "<=", v, ctx));
|
|
137
|
+
break;
|
|
138
|
+
case "in":
|
|
139
|
+
clauses.push(inExpr(expr, v, ctx, false));
|
|
140
|
+
break;
|
|
141
|
+
case "notIn":
|
|
142
|
+
clauses.push(inExpr(expr, v, ctx, true));
|
|
143
|
+
break;
|
|
144
|
+
case "contains":
|
|
145
|
+
clauses.push(containsExpr(field, expr, v, ctx));
|
|
146
|
+
break;
|
|
147
|
+
case "startsWith":
|
|
148
|
+
ctx.params.push(bindable(v));
|
|
149
|
+
clauses.push(`instr(${expr}, ?) = 1`);
|
|
150
|
+
break;
|
|
151
|
+
case "endsWith":
|
|
152
|
+
ctx.params.push(bindable(v));
|
|
153
|
+
ctx.params.push(bindable(v));
|
|
154
|
+
clauses.push(`substr(${expr}, -length(?)) = ?`);
|
|
155
|
+
break;
|
|
156
|
+
case "has":
|
|
157
|
+
clauses.push(hasExpr(field, expr, v, ctx));
|
|
158
|
+
break;
|
|
159
|
+
case "exists":
|
|
160
|
+
clauses.push(existsExpr(field, expr, !!v));
|
|
161
|
+
break;
|
|
162
|
+
default:
|
|
163
|
+
throw new MonliteQueryError(
|
|
164
|
+
`Unknown where operator "${op}" on field "${field}"`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!clauses.length) return "";
|
|
169
|
+
return clauses.length === 1 ? clauses[0] : "(" + clauses.join(" AND ") + ")";
|
|
170
|
+
}
|
|
171
|
+
function eqExpr(expr, v, ctx) {
|
|
172
|
+
if (v === null) return `${expr} IS NULL`;
|
|
173
|
+
ctx.params.push(bindable(v));
|
|
174
|
+
return `${expr} = ?`;
|
|
175
|
+
}
|
|
176
|
+
function notExpr(expr, v, ctx) {
|
|
177
|
+
if (v === null) return `${expr} IS NOT NULL`;
|
|
178
|
+
ctx.params.push(bindable(v));
|
|
179
|
+
return `(${expr} IS NULL OR ${expr} != ?)`;
|
|
180
|
+
}
|
|
181
|
+
function cmp(expr, op, v, ctx) {
|
|
182
|
+
ctx.params.push(bindable(v));
|
|
183
|
+
return `${expr} ${op} ?`;
|
|
184
|
+
}
|
|
185
|
+
function inExpr(expr, arr, ctx, negate) {
|
|
186
|
+
if (!Array.isArray(arr)) {
|
|
187
|
+
throw new MonliteQueryError(
|
|
188
|
+
`${negate ? "notIn" : "in"} expects an array`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (arr.length === 0) return negate ? "1" : "0";
|
|
192
|
+
const placeholders = arr.map((v) => {
|
|
193
|
+
ctx.params.push(bindable(v));
|
|
194
|
+
return "?";
|
|
195
|
+
}).join(", ");
|
|
196
|
+
return negate ? `(${expr} IS NULL OR ${expr} NOT IN (${placeholders}))` : `${expr} IN (${placeholders})`;
|
|
197
|
+
}
|
|
198
|
+
function containsExpr(field, expr, v, ctx) {
|
|
199
|
+
if (isReserved(field)) {
|
|
200
|
+
ctx.params.push(bindable(v));
|
|
201
|
+
return `instr(${expr}, ?) > 0`;
|
|
202
|
+
}
|
|
203
|
+
const path = pathLiteral(field);
|
|
204
|
+
ctx.params.push(bindable(v));
|
|
205
|
+
ctx.params.push(bindable(v));
|
|
206
|
+
return `(CASE WHEN json_type(data, ${path}) = 'array' THEN EXISTS (SELECT 1 FROM json_each(data, ${path}) WHERE value = ?) ELSE instr(${expr}, ?) > 0 END)`;
|
|
207
|
+
}
|
|
208
|
+
function hasExpr(field, expr, v, ctx) {
|
|
209
|
+
ctx.params.push(bindable(v));
|
|
210
|
+
if (isReserved(field)) return `${expr} = ?`;
|
|
211
|
+
return `EXISTS (SELECT 1 FROM json_each(data, ${pathLiteral(field)}) WHERE value = ?)`;
|
|
212
|
+
}
|
|
213
|
+
function existsExpr(field, expr, want) {
|
|
214
|
+
if (isReserved(field)) {
|
|
215
|
+
return want ? `${expr} IS NOT NULL` : `${expr} IS NULL`;
|
|
216
|
+
}
|
|
217
|
+
const path = pathLiteral(field);
|
|
218
|
+
return want ? `json_type(data, ${path}) IS NOT NULL` : `json_type(data, ${path}) IS NULL`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/query/order.ts
|
|
222
|
+
function buildOrderBy(orderBy, onPath) {
|
|
223
|
+
if (!orderBy) return "";
|
|
224
|
+
const list = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
225
|
+
const parts = [];
|
|
226
|
+
for (const obj of list) {
|
|
227
|
+
for (const field of Object.keys(obj)) {
|
|
228
|
+
const dir = obj[field];
|
|
229
|
+
if (dir === void 0) continue;
|
|
230
|
+
if (onPath && !isReserved(field)) onPath(field);
|
|
231
|
+
const d = String(dir).toLowerCase() === "desc" ? "DESC" : "ASC";
|
|
232
|
+
parts.push(`${fieldExpr(field)} ${d}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return parts.length ? "ORDER BY " + parts.join(", ") : "";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/query/path.ts
|
|
239
|
+
function getPath(obj, path) {
|
|
240
|
+
let cur = obj;
|
|
241
|
+
for (const seg of path.split(".")) {
|
|
242
|
+
if (cur == null) return void 0;
|
|
243
|
+
cur = cur[seg];
|
|
244
|
+
}
|
|
245
|
+
return cur;
|
|
246
|
+
}
|
|
247
|
+
function setPath(obj, path, value) {
|
|
248
|
+
const segs = path.split(".");
|
|
249
|
+
let cur = obj;
|
|
250
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
251
|
+
const seg = segs[i];
|
|
252
|
+
if (cur[seg] == null || typeof cur[seg] !== "object") cur[seg] = {};
|
|
253
|
+
cur = cur[seg];
|
|
254
|
+
}
|
|
255
|
+
cur[segs[segs.length - 1]] = value;
|
|
256
|
+
}
|
|
257
|
+
function unsetPath(obj, path) {
|
|
258
|
+
const segs = path.split(".");
|
|
259
|
+
let cur = obj;
|
|
260
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
261
|
+
const seg = segs[i];
|
|
262
|
+
if (cur[seg] == null || typeof cur[seg] !== "object") return;
|
|
263
|
+
cur = cur[seg];
|
|
264
|
+
}
|
|
265
|
+
delete cur[segs[segs.length - 1]];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/query/select.ts
|
|
269
|
+
function project(doc, select) {
|
|
270
|
+
if (!select) return doc;
|
|
271
|
+
const keys = Object.keys(select).filter((k) => select[k]);
|
|
272
|
+
if (!keys.length) return doc;
|
|
273
|
+
const out = {};
|
|
274
|
+
for (const key of keys) {
|
|
275
|
+
const value = getPath(doc, key);
|
|
276
|
+
if (value !== void 0) setPath(out, key, value);
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/query/update.ts
|
|
282
|
+
var UPDATE_OPS = /* @__PURE__ */ new Set(["$set", "$unset", "$inc", "$push", "$pull"]);
|
|
283
|
+
function isUpdateOperators(data) {
|
|
284
|
+
return data != null && typeof data === "object" && Object.keys(data).some((k) => k.startsWith("$"));
|
|
285
|
+
}
|
|
286
|
+
function sameValue(a, b) {
|
|
287
|
+
if (a === b) return true;
|
|
288
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
289
|
+
}
|
|
290
|
+
function applyUpdate(doc, data) {
|
|
291
|
+
const next = structuredClone(doc);
|
|
292
|
+
if (!isUpdateOperators(data)) {
|
|
293
|
+
return Object.assign(next, data);
|
|
294
|
+
}
|
|
295
|
+
for (const key of Object.keys(data)) {
|
|
296
|
+
if (!key.startsWith("$")) {
|
|
297
|
+
throw new MonliteQueryError(
|
|
298
|
+
`Cannot mix update operators with plain field "${key}". Use either a plain object or update operators, not both.`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (!UPDATE_OPS.has(key)) {
|
|
302
|
+
throw new MonliteQueryError(`Unknown update operator "${key}"`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const ops = data;
|
|
306
|
+
if (ops.$set) {
|
|
307
|
+
for (const [path, value] of Object.entries(ops.$set)) setPath(next, path, value);
|
|
308
|
+
}
|
|
309
|
+
if (ops.$inc) {
|
|
310
|
+
for (const [path, by] of Object.entries(ops.$inc)) {
|
|
311
|
+
const cur = getPath(next, path);
|
|
312
|
+
setPath(next, path, (typeof cur === "number" ? cur : 0) + Number(by));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (ops.$push) {
|
|
316
|
+
for (const [path, value] of Object.entries(ops.$push)) {
|
|
317
|
+
const cur = getPath(next, path);
|
|
318
|
+
const arr = Array.isArray(cur) ? cur.slice() : [];
|
|
319
|
+
if (value && typeof value === "object" && Array.isArray(value.$each)) {
|
|
320
|
+
arr.push(...value.$each);
|
|
321
|
+
} else {
|
|
322
|
+
arr.push(value);
|
|
323
|
+
}
|
|
324
|
+
setPath(next, path, arr);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (ops.$pull) {
|
|
328
|
+
for (const [path, value] of Object.entries(ops.$pull)) {
|
|
329
|
+
const cur = getPath(next, path);
|
|
330
|
+
if (Array.isArray(cur)) {
|
|
331
|
+
setPath(
|
|
332
|
+
next,
|
|
333
|
+
path,
|
|
334
|
+
cur.filter((x) => !sameValue(x, value))
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (ops.$unset) {
|
|
340
|
+
for (const path of Object.keys(ops.$unset)) unsetPath(next, path);
|
|
341
|
+
}
|
|
342
|
+
return next;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/aggregation/aggregate.ts
|
|
346
|
+
var ACCUMULATORS = ["_sum", "_avg", "_min", "_max"];
|
|
347
|
+
var SQL_FN = {
|
|
348
|
+
_sum: "SUM",
|
|
349
|
+
_avg: "AVG",
|
|
350
|
+
_min: "MIN",
|
|
351
|
+
_max: "MAX"
|
|
352
|
+
};
|
|
353
|
+
function buildAccumulators(args, onPath) {
|
|
354
|
+
const selects = [];
|
|
355
|
+
const cols = [];
|
|
356
|
+
let i = 0;
|
|
357
|
+
for (const kind of ACCUMULATORS) {
|
|
358
|
+
const selection = args[kind];
|
|
359
|
+
if (!selection) continue;
|
|
360
|
+
for (const field of Object.keys(selection)) {
|
|
361
|
+
if (!selection[field]) continue;
|
|
362
|
+
onPath(field);
|
|
363
|
+
const alias = `agg_${kind.slice(1)}_${i++}`;
|
|
364
|
+
selects.push(`${SQL_FN[kind]}(${fieldExpr(field)}) AS ${alias}`);
|
|
365
|
+
cols.push({ alias, kind, field });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return { selects, cols };
|
|
369
|
+
}
|
|
370
|
+
function aggregate(ctx, args) {
|
|
371
|
+
const params = [];
|
|
372
|
+
const where = buildWhere(args.where, { params, onPath: ctx.onPath });
|
|
373
|
+
const { selects, cols } = buildAccumulators(args, ctx.onPath);
|
|
374
|
+
const allSelects = [`COUNT(*) AS agg_count`, ...selects];
|
|
375
|
+
const sql = `SELECT ${allSelects.join(", ")} FROM "${ctx.table}" WHERE ${where}`;
|
|
376
|
+
const row = ctx.db.prepare(sql).get(...params) ?? {};
|
|
377
|
+
const result = {};
|
|
378
|
+
if (args._count) result._count = row.agg_count ?? 0;
|
|
379
|
+
for (const col of cols) {
|
|
380
|
+
const bucket = result[col.kind] ??= {};
|
|
381
|
+
bucket[col.field] = row[col.alias] ?? null;
|
|
382
|
+
}
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
function groupBy(ctx, args) {
|
|
386
|
+
if (!Array.isArray(args.by) || args.by.length === 0) {
|
|
387
|
+
throw new Error("groupBy requires a non-empty `by` array");
|
|
388
|
+
}
|
|
389
|
+
const params = [];
|
|
390
|
+
const where = buildWhere(args.where, { params, onPath: ctx.onPath });
|
|
391
|
+
const groupExprs = [];
|
|
392
|
+
const selects = [];
|
|
393
|
+
for (const field of args.by) {
|
|
394
|
+
ctx.onPath(field);
|
|
395
|
+
const expr = fieldExpr(field);
|
|
396
|
+
groupExprs.push(expr);
|
|
397
|
+
selects.push(`${expr} AS "${field}"`);
|
|
398
|
+
}
|
|
399
|
+
selects.push(`COUNT(*) AS agg_count`);
|
|
400
|
+
const { selects: accSelects, cols } = buildAccumulators(args, ctx.onPath);
|
|
401
|
+
selects.push(...accSelects);
|
|
402
|
+
let sql = `SELECT ${selects.join(", ")} FROM "${ctx.table}" WHERE ${where} GROUP BY ${groupExprs.join(", ")}`;
|
|
403
|
+
if (args.orderBy) {
|
|
404
|
+
const parts = [];
|
|
405
|
+
for (const key of Object.keys(args.orderBy)) {
|
|
406
|
+
const dir = String(args.orderBy[key]).toLowerCase() === "desc" ? "DESC" : "ASC";
|
|
407
|
+
if (key === "_count") parts.push(`agg_count ${dir}`);
|
|
408
|
+
else parts.push(`${fieldExpr(key)} ${dir}`);
|
|
409
|
+
}
|
|
410
|
+
if (parts.length) sql += ` ORDER BY ${parts.join(", ")}`;
|
|
411
|
+
}
|
|
412
|
+
if (args.take != null) {
|
|
413
|
+
sql += " LIMIT ?";
|
|
414
|
+
params.push(args.take);
|
|
415
|
+
}
|
|
416
|
+
if (args.skip != null) {
|
|
417
|
+
sql += (args.take != null ? "" : " LIMIT -1") + " OFFSET ?";
|
|
418
|
+
params.push(args.skip);
|
|
419
|
+
}
|
|
420
|
+
const rows = ctx.db.prepare(sql).all(...params);
|
|
421
|
+
return rows.map((row) => {
|
|
422
|
+
const out = {};
|
|
423
|
+
for (const field of args.by) out[field] = row[field];
|
|
424
|
+
if (args._count) out._count = row.agg_count;
|
|
425
|
+
for (const col of cols) {
|
|
426
|
+
(out[col.kind] ??= {})[col.field] = row[col.alias] ?? null;
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/collection.ts
|
|
433
|
+
var SELECT_COLS = `_id, data, created_at, updated_at`;
|
|
434
|
+
function stripSystem(obj) {
|
|
435
|
+
const { _id, created_at, updated_at, ...rest } = obj;
|
|
436
|
+
return rest;
|
|
437
|
+
}
|
|
438
|
+
var Collection = class {
|
|
439
|
+
constructor(mon, name) {
|
|
440
|
+
this.mon = mon;
|
|
441
|
+
this.name = name;
|
|
442
|
+
}
|
|
443
|
+
mon;
|
|
444
|
+
name;
|
|
445
|
+
initialized = false;
|
|
446
|
+
trackPath = (path) => this.mon.autoIndexer.track(this.name, path);
|
|
447
|
+
get db() {
|
|
448
|
+
return this.mon.sqlite;
|
|
449
|
+
}
|
|
450
|
+
ensureTable() {
|
|
451
|
+
if (this.initialized) return;
|
|
452
|
+
this.db.exec(
|
|
453
|
+
`CREATE TABLE IF NOT EXISTS "${this.name}" (
|
|
454
|
+
_id TEXT PRIMARY KEY,
|
|
455
|
+
data TEXT NOT NULL,
|
|
456
|
+
created_at INTEGER NOT NULL,
|
|
457
|
+
updated_at INTEGER NOT NULL
|
|
458
|
+
)`
|
|
459
|
+
);
|
|
460
|
+
this.initialized = true;
|
|
461
|
+
}
|
|
462
|
+
rowToDoc(row) {
|
|
463
|
+
const doc = JSON.parse(row.data);
|
|
464
|
+
doc._id = row._id;
|
|
465
|
+
doc.created_at = row.created_at;
|
|
466
|
+
doc.updated_at = row.updated_at;
|
|
467
|
+
return doc;
|
|
468
|
+
}
|
|
469
|
+
prepareInsert(input) {
|
|
470
|
+
const now = Date.now();
|
|
471
|
+
const id = input._id != null ? String(input._id) : objectId();
|
|
472
|
+
const doc = stripSystem(input);
|
|
473
|
+
return {
|
|
474
|
+
_id: id,
|
|
475
|
+
data: JSON.stringify(doc),
|
|
476
|
+
created_at: now,
|
|
477
|
+
updated_at: now
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
/* ----------------------------- create ----------------------------- */
|
|
481
|
+
async create(args) {
|
|
482
|
+
this.ensureTable();
|
|
483
|
+
const row = this.prepareInsert(args.data);
|
|
484
|
+
this.db.prepare(
|
|
485
|
+
`INSERT INTO "${this.name}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)`
|
|
486
|
+
).run(row._id, row.data, row.created_at, row.updated_at);
|
|
487
|
+
return this.rowToDoc(row);
|
|
488
|
+
}
|
|
489
|
+
async createMany(args) {
|
|
490
|
+
this.ensureTable();
|
|
491
|
+
const stmt = this.db.prepare(
|
|
492
|
+
`INSERT INTO "${this.name}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)`
|
|
493
|
+
);
|
|
494
|
+
const insertAll = this.db.transaction((items) => {
|
|
495
|
+
for (const item of items) {
|
|
496
|
+
const row = this.prepareInsert(item);
|
|
497
|
+
stmt.run(row._id, row.data, row.created_at, row.updated_at);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
insertAll(args.data);
|
|
501
|
+
return { count: args.data.length };
|
|
502
|
+
}
|
|
503
|
+
/* ------------------------------ read ------------------------------ */
|
|
504
|
+
async findMany(args = {}) {
|
|
505
|
+
this.ensureTable();
|
|
506
|
+
const params = [];
|
|
507
|
+
const where = buildWhere(args.where, { params, onPath: this.trackPath });
|
|
508
|
+
let sql = `SELECT ${SELECT_COLS} FROM "${this.name}" WHERE ${where}`;
|
|
509
|
+
const order = buildOrderBy(args.orderBy, this.trackPath);
|
|
510
|
+
if (order) sql += " " + order;
|
|
511
|
+
if (args.take != null) {
|
|
512
|
+
sql += " LIMIT ?";
|
|
513
|
+
params.push(args.take);
|
|
514
|
+
}
|
|
515
|
+
if (args.skip != null) {
|
|
516
|
+
sql += (args.take != null ? "" : " LIMIT -1") + " OFFSET ?";
|
|
517
|
+
params.push(args.skip);
|
|
518
|
+
}
|
|
519
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
520
|
+
return rows.map(
|
|
521
|
+
(r) => project(this.rowToDoc(r), args.select)
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
async findFirst(args = {}) {
|
|
525
|
+
const rows = await this.findMany({ ...args, take: 1 });
|
|
526
|
+
return rows[0] ?? null;
|
|
527
|
+
}
|
|
528
|
+
async findById(id) {
|
|
529
|
+
this.ensureTable();
|
|
530
|
+
const row = this.db.prepare(`SELECT ${SELECT_COLS} FROM "${this.name}" WHERE _id = ?`).get(id);
|
|
531
|
+
return row ? this.rowToDoc(row) : null;
|
|
532
|
+
}
|
|
533
|
+
async count(args = {}) {
|
|
534
|
+
this.ensureTable();
|
|
535
|
+
const params = [];
|
|
536
|
+
const where = buildWhere(args.where, { params, onPath: this.trackPath });
|
|
537
|
+
const row = this.db.prepare(`SELECT COUNT(*) AS n FROM "${this.name}" WHERE ${where}`).get(...params);
|
|
538
|
+
return row.n;
|
|
539
|
+
}
|
|
540
|
+
/* ----------------------------- update ----------------------------- */
|
|
541
|
+
runUpdate(where, data, single) {
|
|
542
|
+
this.ensureTable();
|
|
543
|
+
const params = [];
|
|
544
|
+
const clause = buildWhere(where, { params, onPath: this.trackPath });
|
|
545
|
+
let selectSql = `SELECT ${SELECT_COLS} FROM "${this.name}" WHERE ${clause}`;
|
|
546
|
+
if (single) selectSql += " LIMIT 1";
|
|
547
|
+
const rows = this.db.prepare(selectSql).all(...params);
|
|
548
|
+
if (!rows.length) return [];
|
|
549
|
+
const now = Date.now();
|
|
550
|
+
const stmt = this.db.prepare(
|
|
551
|
+
`UPDATE "${this.name}" SET data = ?, updated_at = ? WHERE _id = ?`
|
|
552
|
+
);
|
|
553
|
+
const txn = this.db.transaction(() => {
|
|
554
|
+
const out = [];
|
|
555
|
+
for (const row of rows) {
|
|
556
|
+
const current = JSON.parse(row.data);
|
|
557
|
+
const updated = stripSystem(applyUpdate(current, data));
|
|
558
|
+
stmt.run(JSON.stringify(updated), now, row._id);
|
|
559
|
+
out.push({
|
|
560
|
+
...updated,
|
|
561
|
+
_id: row._id,
|
|
562
|
+
created_at: row.created_at,
|
|
563
|
+
updated_at: now
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return out;
|
|
567
|
+
});
|
|
568
|
+
return txn();
|
|
569
|
+
}
|
|
570
|
+
async update(args) {
|
|
571
|
+
return this.runUpdate(args.where, args.data, true)[0] ?? null;
|
|
572
|
+
}
|
|
573
|
+
async updateMany(args) {
|
|
574
|
+
return { count: this.runUpdate(args.where, args.data, false).length };
|
|
575
|
+
}
|
|
576
|
+
async upsert(args) {
|
|
577
|
+
this.ensureTable();
|
|
578
|
+
const existing = await this.findFirst({ where: args.where });
|
|
579
|
+
if (existing) {
|
|
580
|
+
const updated = await this.update({
|
|
581
|
+
where: { _id: existing._id },
|
|
582
|
+
data: args.update
|
|
583
|
+
});
|
|
584
|
+
return updated;
|
|
585
|
+
}
|
|
586
|
+
return this.create({ data: args.create });
|
|
587
|
+
}
|
|
588
|
+
/* ----------------------------- delete ----------------------------- */
|
|
589
|
+
runDelete(where, single) {
|
|
590
|
+
this.ensureTable();
|
|
591
|
+
const params = [];
|
|
592
|
+
const clause = buildWhere(where, { params, onPath: this.trackPath });
|
|
593
|
+
let selectSql = `SELECT ${SELECT_COLS} FROM "${this.name}" WHERE ${clause}`;
|
|
594
|
+
if (single) selectSql += " LIMIT 1";
|
|
595
|
+
const rows = this.db.prepare(selectSql).all(...params);
|
|
596
|
+
if (!rows.length) return [];
|
|
597
|
+
const stmt = this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`);
|
|
598
|
+
const txn = this.db.transaction(() => {
|
|
599
|
+
for (const row of rows) stmt.run(row._id);
|
|
600
|
+
});
|
|
601
|
+
txn();
|
|
602
|
+
return rows.map((r) => this.rowToDoc(r));
|
|
603
|
+
}
|
|
604
|
+
async delete(args) {
|
|
605
|
+
return this.runDelete(args.where, true)[0] ?? null;
|
|
606
|
+
}
|
|
607
|
+
async deleteMany(args = { where: void 0 }) {
|
|
608
|
+
return { count: this.runDelete(args.where, false).length };
|
|
609
|
+
}
|
|
610
|
+
/* --------------------------- aggregation -------------------------- */
|
|
611
|
+
async aggregate(args = {}) {
|
|
612
|
+
this.ensureTable();
|
|
613
|
+
return aggregate(
|
|
614
|
+
{ db: this.db, table: this.name, onPath: this.trackPath },
|
|
615
|
+
args
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
async groupBy(args) {
|
|
619
|
+
this.ensureTable();
|
|
620
|
+
return groupBy(
|
|
621
|
+
{ db: this.db, table: this.name, onPath: this.trackPath },
|
|
622
|
+
args
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// src/auto-index.ts
|
|
628
|
+
var AutoIndexer = class {
|
|
629
|
+
constructor(db, enabled, threshold) {
|
|
630
|
+
this.db = db;
|
|
631
|
+
this.enabled = enabled;
|
|
632
|
+
this.threshold = threshold;
|
|
633
|
+
}
|
|
634
|
+
db;
|
|
635
|
+
enabled;
|
|
636
|
+
threshold;
|
|
637
|
+
counts = /* @__PURE__ */ new Map();
|
|
638
|
+
created = /* @__PURE__ */ new Set();
|
|
639
|
+
track(collection, path) {
|
|
640
|
+
if (!this.enabled) return;
|
|
641
|
+
const key = `${collection}\0${path}`;
|
|
642
|
+
if (this.created.has(key)) return;
|
|
643
|
+
const next = (this.counts.get(key) ?? 0) + 1;
|
|
644
|
+
this.counts.set(key, next);
|
|
645
|
+
if (next >= this.threshold) {
|
|
646
|
+
this.create(collection, path);
|
|
647
|
+
this.created.add(key);
|
|
648
|
+
this.counts.delete(key);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
create(collection, path) {
|
|
652
|
+
const safe = path.replace(/[^A-Za-z0-9_]+/g, "_");
|
|
653
|
+
const indexName = `idx_${collection}_${safe}`;
|
|
654
|
+
const expr = `json_extract(data, ${pathLiteral(path)})`;
|
|
655
|
+
try {
|
|
656
|
+
this.db.exec(
|
|
657
|
+
`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${collection}"(${expr})`
|
|
658
|
+
);
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/** Forget tracking for a collection (or everything when omitted). */
|
|
663
|
+
reset(collection) {
|
|
664
|
+
if (!collection) {
|
|
665
|
+
this.counts.clear();
|
|
666
|
+
this.created.clear();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const prefix = `${collection}\0`;
|
|
670
|
+
for (const k of [...this.counts.keys()]) {
|
|
671
|
+
if (k.startsWith(prefix)) this.counts.delete(k);
|
|
672
|
+
}
|
|
673
|
+
for (const k of [...this.created.keys()]) {
|
|
674
|
+
if (k.startsWith(prefix)) this.created.delete(k);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// src/db.ts
|
|
680
|
+
function validateName(name) {
|
|
681
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
682
|
+
throw new MonliteError(
|
|
683
|
+
`Invalid collection name "${name}". Names must start with a letter or underscore and contain only letters, digits and underscores.`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function buildTagged(strings, values) {
|
|
688
|
+
let sql = "";
|
|
689
|
+
const params = [];
|
|
690
|
+
strings.forEach((part, i) => {
|
|
691
|
+
sql += part;
|
|
692
|
+
if (i < values.length) {
|
|
693
|
+
sql += "?";
|
|
694
|
+
params.push(bindable(values[i]));
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
return { sql, params };
|
|
698
|
+
}
|
|
699
|
+
var Monlite = class {
|
|
700
|
+
/** The underlying better-sqlite3 connection (escape hatch). */
|
|
701
|
+
sqlite;
|
|
702
|
+
/** @internal */
|
|
703
|
+
autoIndexer;
|
|
704
|
+
collections = /* @__PURE__ */ new Map();
|
|
705
|
+
closed = false;
|
|
706
|
+
constructor(filename, options = {}) {
|
|
707
|
+
const verbose = options.verbose;
|
|
708
|
+
this.sqlite = new Database__default.default(filename, {
|
|
709
|
+
readonly: options.readonly ?? false,
|
|
710
|
+
...verbose ? { verbose: (msg) => verbose(String(msg)) } : {}
|
|
711
|
+
});
|
|
712
|
+
if (!options.readonly && (options.wal ?? true)) {
|
|
713
|
+
this.sqlite.pragma("journal_mode = WAL");
|
|
714
|
+
}
|
|
715
|
+
this.autoIndexer = new AutoIndexer(
|
|
716
|
+
this.sqlite,
|
|
717
|
+
options.autoIndex ?? true,
|
|
718
|
+
options.autoIndexAfter ?? 10
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
/** Get (or lazily create) a typed collection handle. */
|
|
722
|
+
collection(name) {
|
|
723
|
+
this.assertOpen();
|
|
724
|
+
validateName(name);
|
|
725
|
+
let col = this.collections.get(name);
|
|
726
|
+
if (!col) {
|
|
727
|
+
col = new Collection(this, name);
|
|
728
|
+
this.collections.set(name, col);
|
|
729
|
+
}
|
|
730
|
+
return col;
|
|
731
|
+
}
|
|
732
|
+
/** Tagged-template SQL query returning rows. Values are safely parameterized. */
|
|
733
|
+
$queryRaw(strings, ...values) {
|
|
734
|
+
this.assertOpen();
|
|
735
|
+
const { sql, params } = buildTagged(strings, values);
|
|
736
|
+
return Promise.resolve(this.sqlite.prepare(sql).all(...params));
|
|
737
|
+
}
|
|
738
|
+
/** Like {@link $queryRaw} but takes a raw SQL string and positional params. */
|
|
739
|
+
$queryRawUnsafe(sql, ...params) {
|
|
740
|
+
this.assertOpen();
|
|
741
|
+
return Promise.resolve(
|
|
742
|
+
this.sqlite.prepare(sql).all(...params.map(bindable))
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
/** Tagged-template SQL statement returning the number of affected rows. */
|
|
746
|
+
$executeRaw(strings, ...values) {
|
|
747
|
+
this.assertOpen();
|
|
748
|
+
const { sql, params } = buildTagged(strings, values);
|
|
749
|
+
return Promise.resolve(this.sqlite.prepare(sql).run(...params).changes);
|
|
750
|
+
}
|
|
751
|
+
/** Like {@link $executeRaw} but takes a raw SQL string and positional params. */
|
|
752
|
+
$executeRawUnsafe(sql, ...params) {
|
|
753
|
+
this.assertOpen();
|
|
754
|
+
return Promise.resolve(
|
|
755
|
+
this.sqlite.prepare(sql).run(...params.map(bindable)).changes
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Run a function inside a synchronous SQLite transaction. If it throws, the
|
|
760
|
+
* transaction is rolled back.
|
|
761
|
+
*/
|
|
762
|
+
async $transaction(fn) {
|
|
763
|
+
this.assertOpen();
|
|
764
|
+
const txn = this.sqlite.transaction(() => fn(this));
|
|
765
|
+
return txn();
|
|
766
|
+
}
|
|
767
|
+
/** List all collection (table) names. */
|
|
768
|
+
$collections() {
|
|
769
|
+
this.assertOpen();
|
|
770
|
+
const rows = this.sqlite.prepare(
|
|
771
|
+
`SELECT name FROM sqlite_master
|
|
772
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
773
|
+
ORDER BY name`
|
|
774
|
+
).all();
|
|
775
|
+
return Promise.resolve(rows.map((r) => r.name));
|
|
776
|
+
}
|
|
777
|
+
/** Drop a collection and all of its data. */
|
|
778
|
+
$drop(name) {
|
|
779
|
+
this.assertOpen();
|
|
780
|
+
validateName(name);
|
|
781
|
+
this.sqlite.exec(`DROP TABLE IF EXISTS "${name}"`);
|
|
782
|
+
this.collections.delete(name);
|
|
783
|
+
this.autoIndexer.reset(name);
|
|
784
|
+
return Promise.resolve();
|
|
785
|
+
}
|
|
786
|
+
/** Drop every collection in the database. */
|
|
787
|
+
async $dropAll() {
|
|
788
|
+
for (const name of await this.$collections()) await this.$drop(name);
|
|
789
|
+
}
|
|
790
|
+
/** Close the underlying SQLite connection. */
|
|
791
|
+
$disconnect() {
|
|
792
|
+
if (!this.closed) {
|
|
793
|
+
this.closed = true;
|
|
794
|
+
this.sqlite.close();
|
|
795
|
+
}
|
|
796
|
+
return Promise.resolve();
|
|
797
|
+
}
|
|
798
|
+
assertOpen() {
|
|
799
|
+
if (this.closed) throw new MonliteError("Database connection is closed");
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
function createDb(filename, options) {
|
|
803
|
+
return new Monlite(filename, options);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
exports.Collection = Collection;
|
|
807
|
+
exports.Monlite = Monlite;
|
|
808
|
+
exports.MonliteError = MonliteError;
|
|
809
|
+
exports.MonliteQueryError = MonliteQueryError;
|
|
810
|
+
exports.createDb = createDb;
|
|
811
|
+
exports.isObjectId = isObjectId;
|
|
812
|
+
exports.objectId = objectId;
|
|
813
|
+
//# sourceMappingURL=index.cjs.map
|
|
814
|
+
//# sourceMappingURL=index.cjs.map
|