sqlite-zod-orm 3.23.0 → 3.25.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/dist/index.js +518 -127
- package/package.json +57 -56
- package/src/context.ts +6 -0
- package/src/crud.ts +11 -9
- package/src/database.ts +127 -109
- package/src/query.ts +5 -4
package/dist/index.js
CHANGED
|
@@ -13,6 +13,389 @@ var __export = (target, all) => {
|
|
|
13
13
|
// src/database.ts
|
|
14
14
|
import { Database as SqliteDatabase } from "bun:sqlite";
|
|
15
15
|
|
|
16
|
+
// node_modules/measure-fn/index.ts
|
|
17
|
+
var toAlpha = (num) => {
|
|
18
|
+
let result = "";
|
|
19
|
+
let n = num;
|
|
20
|
+
do {
|
|
21
|
+
result = String.fromCharCode(97 + n % 26) + result;
|
|
22
|
+
n = Math.floor(n / 26) - 1;
|
|
23
|
+
} while (n >= 0);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
var maxResultLen = 80;
|
|
27
|
+
var safeStringify = (value) => {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return "";
|
|
30
|
+
if (value === null)
|
|
31
|
+
return "null";
|
|
32
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
33
|
+
return String(value);
|
|
34
|
+
if (typeof value === "function")
|
|
35
|
+
return `[Function: ${value.name || "anonymous"}]`;
|
|
36
|
+
if (typeof value === "symbol")
|
|
37
|
+
return value.toString();
|
|
38
|
+
if (typeof value === "string") {
|
|
39
|
+
const q = JSON.stringify(value);
|
|
40
|
+
return q.length > maxResultLen ? q.slice(0, maxResultLen - 1) + '\u2026"' : q;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const seen = new WeakSet;
|
|
44
|
+
const str = JSON.stringify(value, (_key, val) => {
|
|
45
|
+
if (typeof val === "object" && val !== null) {
|
|
46
|
+
if (seen.has(val))
|
|
47
|
+
return "[Circular]";
|
|
48
|
+
seen.add(val);
|
|
49
|
+
}
|
|
50
|
+
if (typeof val === "function")
|
|
51
|
+
return `[Function: ${val.name || "anonymous"}]`;
|
|
52
|
+
if (typeof val === "bigint")
|
|
53
|
+
return `${val}n`;
|
|
54
|
+
return val;
|
|
55
|
+
});
|
|
56
|
+
return str.length > maxResultLen ? str.slice(0, maxResultLen) + "\u2026" : str;
|
|
57
|
+
} catch {
|
|
58
|
+
return String(value);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var formatDuration = (ms) => {
|
|
62
|
+
if (ms < 1000)
|
|
63
|
+
return `${ms.toFixed(2)}ms`;
|
|
64
|
+
if (ms < 60000)
|
|
65
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
66
|
+
const mins = Math.floor(ms / 60000);
|
|
67
|
+
const secs = Math.round(ms % 60000 / 1000);
|
|
68
|
+
return `${mins}m ${secs}s`;
|
|
69
|
+
};
|
|
70
|
+
var timestamps = process.env.MEASURE_TIMESTAMPS === "1" || process.env.MEASURE_TIMESTAMPS === "true";
|
|
71
|
+
var ts = () => {
|
|
72
|
+
if (!timestamps)
|
|
73
|
+
return "";
|
|
74
|
+
const now = new Date;
|
|
75
|
+
const h = String(now.getHours()).padStart(2, "0");
|
|
76
|
+
const m = String(now.getMinutes()).padStart(2, "0");
|
|
77
|
+
const s = String(now.getSeconds()).padStart(2, "0");
|
|
78
|
+
const ms = String(now.getMilliseconds()).padStart(3, "0");
|
|
79
|
+
return `[${h}:${m}:${s}.${ms}] `;
|
|
80
|
+
};
|
|
81
|
+
var silent = process.env.MEASURE_SILENT === "1" || process.env.MEASURE_SILENT === "true";
|
|
82
|
+
var logger = null;
|
|
83
|
+
var buildActionLabel = (actionInternal) => {
|
|
84
|
+
return typeof actionInternal === "object" && actionInternal !== null && "label" in actionInternal ? String(actionInternal.label) : String(actionInternal);
|
|
85
|
+
};
|
|
86
|
+
var extractBudget = (actionInternal) => {
|
|
87
|
+
if (typeof actionInternal !== "object" || actionInternal === null)
|
|
88
|
+
return;
|
|
89
|
+
if ("budget" in actionInternal)
|
|
90
|
+
return Number(actionInternal.budget);
|
|
91
|
+
return;
|
|
92
|
+
};
|
|
93
|
+
var extractMeta = (actionInternal) => {
|
|
94
|
+
if (typeof actionInternal !== "object" || actionInternal === null)
|
|
95
|
+
return;
|
|
96
|
+
const details = { ...actionInternal };
|
|
97
|
+
if ("label" in details)
|
|
98
|
+
delete details.label;
|
|
99
|
+
if ("budget" in details)
|
|
100
|
+
delete details.budget;
|
|
101
|
+
if (Object.keys(details).length === 0)
|
|
102
|
+
return;
|
|
103
|
+
return details;
|
|
104
|
+
};
|
|
105
|
+
var formatMeta = (meta) => {
|
|
106
|
+
if (!meta)
|
|
107
|
+
return "";
|
|
108
|
+
const params = Object.entries(meta).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(" ");
|
|
109
|
+
return ` (${params})`;
|
|
110
|
+
};
|
|
111
|
+
var emit = (event, prefix) => {
|
|
112
|
+
if (silent)
|
|
113
|
+
return;
|
|
114
|
+
if (logger) {
|
|
115
|
+
logger(event);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
defaultLogger(event, prefix);
|
|
119
|
+
};
|
|
120
|
+
var defaultLogger = (event, prefix) => {
|
|
121
|
+
const pfx = prefix ? `${prefix}:` : "";
|
|
122
|
+
const id = `[${pfx}${event.id}]`;
|
|
123
|
+
const t = ts();
|
|
124
|
+
switch (event.type) {
|
|
125
|
+
case "start":
|
|
126
|
+
console.log(`${t}${id} ... ${event.label}${formatMeta(event.meta)}`);
|
|
127
|
+
break;
|
|
128
|
+
case "success": {
|
|
129
|
+
const resultStr = event.result !== undefined ? safeStringify(event.result) : "";
|
|
130
|
+
const arrow = resultStr ? ` \u2192 ${resultStr}` : "";
|
|
131
|
+
const budgetWarn = event.budget && event.duration > event.budget ? ` \u26A0 OVER BUDGET (${formatDuration(event.budget)})` : "";
|
|
132
|
+
console.log(`${t}${id} \u2713 ${event.label} ${formatDuration(event.duration)}${arrow}${budgetWarn}`);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case "error": {
|
|
136
|
+
const errorMsg = event.error instanceof Error ? event.error.message : String(event.error);
|
|
137
|
+
const budgetWarn = event.budget && event.duration > event.budget ? ` \u26A0 OVER BUDGET (${formatDuration(event.budget)})` : "";
|
|
138
|
+
console.log(`${t}${id} \u2717 ${event.label} ${formatDuration(event.duration)} (${errorMsg})${budgetWarn}`);
|
|
139
|
+
if (event.error instanceof Error) {
|
|
140
|
+
console.error(`${id}`, event.error.stack ?? event.error.message);
|
|
141
|
+
if (event.error.cause) {
|
|
142
|
+
console.error(`${id} Cause:`, event.error.cause);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
console.error(`${id}`, event.error);
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case "annotation":
|
|
150
|
+
console.log(`${t}${id} = ${event.label}${formatMeta(event.meta)}`);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var createNestedResolver = (isAsync, fullIdChain, childCounterRef, depth, resolver, prefix) => {
|
|
155
|
+
return (...args) => {
|
|
156
|
+
const label = args[0];
|
|
157
|
+
const fn = args[1];
|
|
158
|
+
if (typeof fn === "function") {
|
|
159
|
+
const childParentChain = [...fullIdChain, childCounterRef.value++];
|
|
160
|
+
return resolver(fn, label, childParentChain, depth + 1);
|
|
161
|
+
} else {
|
|
162
|
+
emit({
|
|
163
|
+
type: "annotation",
|
|
164
|
+
id: fullIdChain.join("-"),
|
|
165
|
+
label: buildActionLabel(label),
|
|
166
|
+
depth: depth + 1,
|
|
167
|
+
meta: extractMeta(label)
|
|
168
|
+
}, prefix);
|
|
169
|
+
return isAsync ? Promise.resolve(null) : null;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
var globalRootCounter = 0;
|
|
174
|
+
var createMeasureImpl = (prefix, counterRef) => {
|
|
175
|
+
const counter = counterRef ?? { get value() {
|
|
176
|
+
return globalRootCounter;
|
|
177
|
+
}, set value(v) {
|
|
178
|
+
globalRootCounter = v;
|
|
179
|
+
} };
|
|
180
|
+
let _lastError = null;
|
|
181
|
+
const _measureInternal = async (fnInternal, actionInternal, parentIdChain, depth) => {
|
|
182
|
+
const start = performance.now();
|
|
183
|
+
const childCounterRef = { value: 0 };
|
|
184
|
+
const label = buildActionLabel(actionInternal);
|
|
185
|
+
const budget = extractBudget(actionInternal);
|
|
186
|
+
const currentId = toAlpha(parentIdChain.pop() ?? 0);
|
|
187
|
+
const fullIdChain = [...parentIdChain, currentId];
|
|
188
|
+
const idStr = fullIdChain.join("-");
|
|
189
|
+
emit({
|
|
190
|
+
type: "start",
|
|
191
|
+
id: idStr,
|
|
192
|
+
label,
|
|
193
|
+
depth,
|
|
194
|
+
meta: extractMeta(actionInternal)
|
|
195
|
+
}, prefix);
|
|
196
|
+
const measureForNextLevel = createNestedResolver(true, fullIdChain, childCounterRef, depth, _measureInternal, prefix);
|
|
197
|
+
try {
|
|
198
|
+
const result = await fnInternal(measureForNextLevel);
|
|
199
|
+
const duration = performance.now() - start;
|
|
200
|
+
emit({ type: "success", id: idStr, label, depth, duration, result, budget }, prefix);
|
|
201
|
+
return result;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const duration = performance.now() - start;
|
|
204
|
+
emit({ type: "error", id: idStr, label, depth, duration, error, budget }, prefix);
|
|
205
|
+
_lastError = error;
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const _measureInternalSync = (fnInternal, actionInternal, parentIdChain, depth) => {
|
|
210
|
+
const start = performance.now();
|
|
211
|
+
const childCounterRef = { value: 0 };
|
|
212
|
+
const label = buildActionLabel(actionInternal);
|
|
213
|
+
const hasNested = fnInternal.length > 0;
|
|
214
|
+
const budget = extractBudget(actionInternal);
|
|
215
|
+
const currentId = toAlpha(parentIdChain.pop() ?? 0);
|
|
216
|
+
const fullIdChain = [...parentIdChain, currentId];
|
|
217
|
+
const idStr = fullIdChain.join("-");
|
|
218
|
+
if (hasNested) {
|
|
219
|
+
emit({
|
|
220
|
+
type: "start",
|
|
221
|
+
id: idStr,
|
|
222
|
+
label,
|
|
223
|
+
depth,
|
|
224
|
+
meta: extractMeta(actionInternal)
|
|
225
|
+
}, prefix);
|
|
226
|
+
}
|
|
227
|
+
const measureForNextLevel = createNestedResolver(false, fullIdChain, childCounterRef, depth, _measureInternalSync, prefix);
|
|
228
|
+
try {
|
|
229
|
+
const result = fnInternal(measureForNextLevel);
|
|
230
|
+
const duration = performance.now() - start;
|
|
231
|
+
emit({ type: "success", id: idStr, label, depth, duration, result, budget }, prefix);
|
|
232
|
+
return result;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const duration = performance.now() - start;
|
|
235
|
+
emit({ type: "error", id: idStr, label, depth, duration, error, budget }, prefix);
|
|
236
|
+
_lastError = error;
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const measureFn = async (arg1, arg2) => {
|
|
241
|
+
if (typeof arg2 === "function") {
|
|
242
|
+
return _measureInternal(arg2, arg1, [counter.value++], 0);
|
|
243
|
+
} else {
|
|
244
|
+
const currentId = toAlpha(counter.value++);
|
|
245
|
+
emit({
|
|
246
|
+
type: "annotation",
|
|
247
|
+
id: currentId,
|
|
248
|
+
label: buildActionLabel(arg1),
|
|
249
|
+
depth: 0,
|
|
250
|
+
meta: extractMeta(arg1)
|
|
251
|
+
}, prefix);
|
|
252
|
+
return Promise.resolve(null);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
measureFn.timed = async (arg1, arg2) => {
|
|
256
|
+
const start = performance.now();
|
|
257
|
+
const result = await measureFn(arg1, arg2);
|
|
258
|
+
const duration = performance.now() - start;
|
|
259
|
+
return { result, duration };
|
|
260
|
+
};
|
|
261
|
+
measureFn.retry = async (label, opts, fn) => {
|
|
262
|
+
const attempts = opts.attempts ?? 3;
|
|
263
|
+
const delay = opts.delay ?? 1000;
|
|
264
|
+
const backoff = opts.backoff ?? 1;
|
|
265
|
+
const lbl = buildActionLabel(label);
|
|
266
|
+
const budget = extractBudget(label);
|
|
267
|
+
for (let i = 0;i < attempts; i++) {
|
|
268
|
+
const attempt = i + 1;
|
|
269
|
+
const attemptLabel = `${lbl} [${attempt}/${attempts}]`;
|
|
270
|
+
const start = performance.now();
|
|
271
|
+
const currentId = toAlpha(counter.value++);
|
|
272
|
+
emit({
|
|
273
|
+
type: "start",
|
|
274
|
+
id: currentId,
|
|
275
|
+
label: attemptLabel,
|
|
276
|
+
depth: 0,
|
|
277
|
+
meta: extractMeta(label)
|
|
278
|
+
}, prefix);
|
|
279
|
+
try {
|
|
280
|
+
const result = await fn();
|
|
281
|
+
const duration = performance.now() - start;
|
|
282
|
+
emit({ type: "success", id: currentId, label: attemptLabel, depth: 0, duration, result, budget }, prefix);
|
|
283
|
+
return result;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const duration = performance.now() - start;
|
|
286
|
+
emit({ type: "error", id: currentId, label: attemptLabel, depth: 0, duration, error, budget }, prefix);
|
|
287
|
+
if (attempt < attempts) {
|
|
288
|
+
await new Promise((r) => setTimeout(r, delay * Math.pow(backoff, i)));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
};
|
|
294
|
+
measureFn.assert = async (arg1, arg2) => {
|
|
295
|
+
const result = await measureFn(arg1, arg2);
|
|
296
|
+
if (result === null) {
|
|
297
|
+
const cause = _lastError;
|
|
298
|
+
_lastError = null;
|
|
299
|
+
throw new Error(`measure.assert: "${buildActionLabel(arg1)}" failed`, { cause });
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
};
|
|
303
|
+
measureFn.wrap = (label, fn) => {
|
|
304
|
+
return (...args) => measureFn(label, () => fn(...args));
|
|
305
|
+
};
|
|
306
|
+
measureFn.batch = async (label, items, fn, opts) => {
|
|
307
|
+
const lbl = buildActionLabel(label);
|
|
308
|
+
const total = items.length;
|
|
309
|
+
const every = opts?.every ?? Math.max(1, Math.ceil(total / 5));
|
|
310
|
+
const currentId = toAlpha(counter.value++);
|
|
311
|
+
const startTime = performance.now();
|
|
312
|
+
emit({
|
|
313
|
+
type: "start",
|
|
314
|
+
id: currentId,
|
|
315
|
+
label: `${lbl} (${total} items)`,
|
|
316
|
+
depth: 0,
|
|
317
|
+
meta: extractMeta(label)
|
|
318
|
+
}, prefix);
|
|
319
|
+
const results = [];
|
|
320
|
+
for (let i = 0;i < items.length; i++) {
|
|
321
|
+
try {
|
|
322
|
+
results.push(await fn(items[i], i));
|
|
323
|
+
} catch {
|
|
324
|
+
results.push(null);
|
|
325
|
+
}
|
|
326
|
+
if ((i + 1) % every === 0 && i + 1 < total) {
|
|
327
|
+
const elapsed = (performance.now() - startTime) / 1000;
|
|
328
|
+
const rate = ((i + 1) / elapsed).toFixed(0);
|
|
329
|
+
emit({
|
|
330
|
+
type: "annotation",
|
|
331
|
+
id: currentId,
|
|
332
|
+
label: `${i + 1}/${total} (${elapsed.toFixed(1)}s, ${rate}/s)`,
|
|
333
|
+
depth: 0
|
|
334
|
+
}, prefix);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const duration = performance.now() - startTime;
|
|
338
|
+
const budget = extractBudget(label);
|
|
339
|
+
emit({
|
|
340
|
+
type: "success",
|
|
341
|
+
id: currentId,
|
|
342
|
+
label: `${lbl} (${total} items)`,
|
|
343
|
+
depth: 0,
|
|
344
|
+
duration,
|
|
345
|
+
result: `${results.filter((r) => r !== null).length}/${total} ok`,
|
|
346
|
+
budget
|
|
347
|
+
}, prefix);
|
|
348
|
+
return results;
|
|
349
|
+
};
|
|
350
|
+
const measureSyncFn = (arg1, arg2) => {
|
|
351
|
+
if (typeof arg2 === "function") {
|
|
352
|
+
return _measureInternalSync(arg2, arg1, [counter.value++], 0);
|
|
353
|
+
} else {
|
|
354
|
+
const currentId = toAlpha(counter.value++);
|
|
355
|
+
emit({
|
|
356
|
+
type: "annotation",
|
|
357
|
+
id: currentId,
|
|
358
|
+
label: buildActionLabel(arg1),
|
|
359
|
+
depth: 0,
|
|
360
|
+
meta: extractMeta(arg1)
|
|
361
|
+
}, prefix);
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
measureSyncFn.timed = (arg1, arg2) => {
|
|
366
|
+
const start = performance.now();
|
|
367
|
+
const result = measureSyncFn(arg1, arg2);
|
|
368
|
+
const duration = performance.now() - start;
|
|
369
|
+
return { result, duration };
|
|
370
|
+
};
|
|
371
|
+
measureSyncFn.assert = (arg1, arg2) => {
|
|
372
|
+
const result = measureSyncFn(arg1, arg2);
|
|
373
|
+
if (result === null) {
|
|
374
|
+
const cause = _lastError;
|
|
375
|
+
_lastError = null;
|
|
376
|
+
throw new Error(`measureSync.assert: "${buildActionLabel(arg1)}" failed`, { cause });
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
};
|
|
380
|
+
measureSyncFn.wrap = (label, fn) => {
|
|
381
|
+
return (...args) => measureSyncFn(label, () => fn(...args));
|
|
382
|
+
};
|
|
383
|
+
return { measure: measureFn, measureSync: measureSyncFn };
|
|
384
|
+
};
|
|
385
|
+
var globalInstance = createMeasureImpl();
|
|
386
|
+
var measure = globalInstance.measure;
|
|
387
|
+
var measureSync = globalInstance.measureSync;
|
|
388
|
+
var createMeasure = (scopePrefix) => {
|
|
389
|
+
const scopeCounter = { value: 0 };
|
|
390
|
+
const scoped = createMeasureImpl(scopePrefix, scopeCounter);
|
|
391
|
+
return {
|
|
392
|
+
...scoped,
|
|
393
|
+
resetCounter: () => {
|
|
394
|
+
scopeCounter.value = 0;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
};
|
|
398
|
+
|
|
16
399
|
// node_modules/zod/v3/external.js
|
|
17
400
|
var exports_external = {};
|
|
18
401
|
__export(exports_external, {
|
|
@@ -4837,12 +5220,12 @@ function executeProxyQuery(schemas, callback, executor) {
|
|
|
4837
5220
|
function createQueryBuilder(ctx, entityName, initialCols) {
|
|
4838
5221
|
const schema = ctx.schemas[entityName];
|
|
4839
5222
|
const executor = (sql, params, raw) => {
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
return rows;
|
|
4845
|
-
|
|
5223
|
+
return ctx._m(`SQL: ${sql.slice(0, 60)}`, () => {
|
|
5224
|
+
const rows = ctx.db.query(sql).all(...params);
|
|
5225
|
+
if (raw)
|
|
5226
|
+
return rows;
|
|
5227
|
+
return rows.map((row) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
|
|
5228
|
+
});
|
|
4846
5229
|
};
|
|
4847
5230
|
const singleExecutor = (sql, params, raw) => {
|
|
4848
5231
|
const results = executor(sql, params, raw);
|
|
@@ -5020,9 +5403,9 @@ function insert(ctx, entityName, data) {
|
|
|
5020
5403
|
let inputData = { ...data };
|
|
5021
5404
|
const hooks = ctx.hooks[entityName];
|
|
5022
5405
|
if (hooks?.beforeInsert) {
|
|
5023
|
-
const
|
|
5024
|
-
if (
|
|
5025
|
-
inputData =
|
|
5406
|
+
const result = hooks.beforeInsert(inputData);
|
|
5407
|
+
if (result)
|
|
5408
|
+
inputData = result;
|
|
5026
5409
|
}
|
|
5027
5410
|
const validatedData = asZodObject(schema).passthrough().parse(inputData);
|
|
5028
5411
|
const transformed = transformForStorage(validatedData);
|
|
@@ -5034,10 +5417,12 @@ function insert(ctx, entityName, data) {
|
|
|
5034
5417
|
const columns = Object.keys(transformed);
|
|
5035
5418
|
const quotedCols = columns.map((c) => `"${c}"`);
|
|
5036
5419
|
const sql = columns.length === 0 ? `INSERT INTO "${entityName}" DEFAULT VALUES` : `INSERT INTO "${entityName}" (${quotedCols.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5420
|
+
let lastId = 0;
|
|
5421
|
+
ctx._m(`SQL: ${sql.slice(0, 40)}`, () => {
|
|
5422
|
+
const result = ctx.db.query(sql).run(...Object.values(transformed));
|
|
5423
|
+
lastId = result.lastInsertRowid;
|
|
5424
|
+
});
|
|
5425
|
+
const newEntity = getById(ctx, entityName, lastId);
|
|
5041
5426
|
if (!newEntity)
|
|
5042
5427
|
throw new Error("Failed to retrieve entity after insertion");
|
|
5043
5428
|
if (hooks?.afterInsert)
|
|
@@ -5062,9 +5447,9 @@ function update(ctx, entityName, id, data) {
|
|
|
5062
5447
|
}
|
|
5063
5448
|
const setClause = Object.keys(transformed).map((key) => `"${key}" = ?`).join(", ");
|
|
5064
5449
|
const sql = `UPDATE "${entityName}" SET ${setClause} WHERE id = ?`;
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5450
|
+
ctx._m(`SQL: UPDATE ${entityName} SET ...`, () => {
|
|
5451
|
+
ctx.db.query(sql).run(...Object.values(transformed), id);
|
|
5452
|
+
});
|
|
5068
5453
|
const updated = getById(ctx, entityName, id);
|
|
5069
5454
|
if (hooks?.afterUpdate && updated)
|
|
5070
5455
|
hooks.afterUpdate(updated);
|
|
@@ -5133,15 +5518,11 @@ function deleteWhere(ctx, entityName, conditions) {
|
|
|
5133
5518
|
if (ctx.softDeletes) {
|
|
5134
5519
|
const now = new Date().toISOString();
|
|
5135
5520
|
const sql2 = `UPDATE "${entityName}" SET "deletedAt" = ? ${clause}`;
|
|
5136
|
-
|
|
5137
|
-
console.log("[satidb]", sql2, [now, ...values]);
|
|
5138
|
-
const result2 = ctx.db.query(sql2).run(now, ...values);
|
|
5521
|
+
const result2 = ctx._m(`SQL: ${sql2.slice(0, 50)}`, () => ctx.db.query(sql2).run(now, ...values));
|
|
5139
5522
|
return result2.changes ?? 0;
|
|
5140
5523
|
}
|
|
5141
5524
|
const sql = `DELETE FROM "${entityName}" ${clause}`;
|
|
5142
|
-
|
|
5143
|
-
console.log("[satidb]", sql, values);
|
|
5144
|
-
const result = ctx.db.query(sql).run(...values);
|
|
5525
|
+
const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx.db.query(sql).run(...values));
|
|
5145
5526
|
return result.changes ?? 0;
|
|
5146
5527
|
}
|
|
5147
5528
|
function createDeleteBuilder(ctx, entityName) {
|
|
@@ -5265,7 +5646,15 @@ class _Database {
|
|
|
5265
5646
|
_changeWatermark = 0;
|
|
5266
5647
|
_pollTimer = null;
|
|
5267
5648
|
_pollInterval;
|
|
5649
|
+
_measure;
|
|
5650
|
+
_m(label, fn) {
|
|
5651
|
+
if (this._debug)
|
|
5652
|
+
return this._measure.measureSync.assert(label, fn);
|
|
5653
|
+
return fn();
|
|
5654
|
+
}
|
|
5268
5655
|
constructor(dbFile, schemas, options = {}) {
|
|
5656
|
+
this._debug = options.debug === true;
|
|
5657
|
+
this._measure = createMeasure("satidb");
|
|
5269
5658
|
this.db = new SqliteDatabase(dbFile);
|
|
5270
5659
|
if (options.wal !== false)
|
|
5271
5660
|
this.db.run("PRAGMA journal_mode = WAL");
|
|
@@ -5275,7 +5664,6 @@ class _Database {
|
|
|
5275
5664
|
this._reactive = options.reactive !== false;
|
|
5276
5665
|
this._timestamps = options.timestamps === true;
|
|
5277
5666
|
this._softDeletes = options.softDeletes === true;
|
|
5278
|
-
this._debug = options.debug === true;
|
|
5279
5667
|
this._pollInterval = options.pollInterval ?? 100;
|
|
5280
5668
|
this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
|
|
5281
5669
|
this._ctx = {
|
|
@@ -5289,74 +5677,77 @@ class _Database {
|
|
|
5289
5677
|
softDeletes: this._softDeletes,
|
|
5290
5678
|
hooks: options.hooks ?? {},
|
|
5291
5679
|
computed: options.computed ?? {},
|
|
5292
|
-
cascade: options.cascade ?? {}
|
|
5680
|
+
cascade: options.cascade ?? {},
|
|
5681
|
+
_m: (label, fn) => this._m(label, fn)
|
|
5293
5682
|
};
|
|
5294
|
-
this.initializeTables();
|
|
5683
|
+
this._m("Init tables", () => this.initializeTables());
|
|
5295
5684
|
if (this._reactive)
|
|
5296
|
-
this.initializeChangeTracking();
|
|
5297
|
-
this.runMigrations();
|
|
5685
|
+
this._m("Change tracking", () => this.initializeChangeTracking());
|
|
5686
|
+
this._m("Run migrations", () => this.runMigrations());
|
|
5298
5687
|
if (options.indexes)
|
|
5299
|
-
this.createIndexes(options.indexes);
|
|
5688
|
+
this._m("Create indexes", () => this.createIndexes(options.indexes));
|
|
5300
5689
|
if (options.unique)
|
|
5301
|
-
this.createUniqueConstraints(options.unique);
|
|
5690
|
+
this._m("Unique constraints", () => this.createUniqueConstraints(options.unique));
|
|
5302
5691
|
for (const entityName of Object.keys(schemas)) {
|
|
5303
5692
|
const key = entityName;
|
|
5304
5693
|
const accessor = {
|
|
5305
|
-
insert: (data) => insert(this._ctx, entityName, data),
|
|
5306
|
-
insertMany: (rows) => insertMany(this._ctx, entityName, rows),
|
|
5694
|
+
insert: (data) => this._m(`${entityName}.insert`, () => insert(this._ctx, entityName, data)),
|
|
5695
|
+
insertMany: (rows) => this._m(`${entityName}.insertMany(${rows.length})`, () => insertMany(this._ctx, entityName, rows)),
|
|
5307
5696
|
update: (idOrData, data) => {
|
|
5308
5697
|
if (typeof idOrData === "number")
|
|
5309
|
-
return update(this._ctx, entityName, idOrData, data);
|
|
5698
|
+
return this._m(`${entityName}.update(${idOrData})`, () => update(this._ctx, entityName, idOrData, data));
|
|
5310
5699
|
return createUpdateBuilder(this._ctx, entityName, idOrData);
|
|
5311
5700
|
},
|
|
5312
|
-
upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
|
|
5313
|
-
upsertMany: (rows, conditions) => upsertMany(this._ctx, entityName, rows, conditions),
|
|
5314
|
-
findOrCreate: (conditions, defaults) => findOrCreate(this._ctx, entityName, conditions, defaults),
|
|
5701
|
+
upsert: (conditions, data) => this._m(`${entityName}.upsert`, () => upsert(this._ctx, entityName, data, conditions)),
|
|
5702
|
+
upsertMany: (rows, conditions) => this._m(`${entityName}.upsertMany(${rows.length})`, () => upsertMany(this._ctx, entityName, rows, conditions)),
|
|
5703
|
+
findOrCreate: (conditions, defaults) => this._m(`${entityName}.findOrCreate`, () => findOrCreate(this._ctx, entityName, conditions, defaults)),
|
|
5315
5704
|
delete: (id) => {
|
|
5316
5705
|
if (typeof id === "number") {
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
if (
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5706
|
+
return this._m(`${entityName}.delete(${id})`, () => {
|
|
5707
|
+
const hooks = this._ctx.hooks[entityName];
|
|
5708
|
+
if (hooks?.beforeDelete) {
|
|
5709
|
+
const result = hooks.beforeDelete(id);
|
|
5710
|
+
if (result === false)
|
|
5711
|
+
return;
|
|
5712
|
+
}
|
|
5713
|
+
const cascadeTargets = this._ctx.cascade[entityName];
|
|
5714
|
+
if (cascadeTargets) {
|
|
5715
|
+
for (const childTable of cascadeTargets) {
|
|
5716
|
+
const rel = this._ctx.relationships.find((r) => r.type === "belongs-to" && r.from === childTable && r.to === entityName);
|
|
5717
|
+
if (rel) {
|
|
5718
|
+
if (this._softDeletes) {
|
|
5719
|
+
const now = new Date().toISOString();
|
|
5720
|
+
this.db.query(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
|
|
5721
|
+
} else {
|
|
5722
|
+
this.db.query(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
|
|
5723
|
+
}
|
|
5333
5724
|
}
|
|
5334
5725
|
}
|
|
5335
5726
|
}
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5727
|
+
if (this._softDeletes) {
|
|
5728
|
+
const now = new Date().toISOString();
|
|
5729
|
+
this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
|
|
5730
|
+
if (hooks?.afterDelete)
|
|
5731
|
+
hooks.afterDelete(id);
|
|
5732
|
+
return;
|
|
5733
|
+
}
|
|
5734
|
+
return deleteEntity(this._ctx, entityName, id);
|
|
5735
|
+
});
|
|
5345
5736
|
}
|
|
5346
5737
|
return createDeleteBuilder(this._ctx, entityName);
|
|
5347
5738
|
},
|
|
5348
5739
|
restore: (id) => {
|
|
5349
5740
|
if (!this._softDeletes)
|
|
5350
5741
|
throw new Error("restore() requires softDeletes: true");
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5742
|
+
this._m(`${entityName}.restore(${id})`, () => {
|
|
5743
|
+
this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
|
|
5744
|
+
});
|
|
5354
5745
|
},
|
|
5355
5746
|
select: (...cols) => createQueryBuilder(this._ctx, entityName, cols),
|
|
5356
|
-
count: () => {
|
|
5747
|
+
count: () => this._m(`${entityName}.count`, () => {
|
|
5357
5748
|
const row = this.db.query(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ""}`).get();
|
|
5358
5749
|
return row?.count ?? 0;
|
|
5359
|
-
},
|
|
5750
|
+
}),
|
|
5360
5751
|
on: (event, callback) => {
|
|
5361
5752
|
return this._registerListener(entityName, event, callback);
|
|
5362
5753
|
},
|
|
@@ -5501,28 +5892,22 @@ class _Database {
|
|
|
5501
5892
|
this.db.query('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
|
|
5502
5893
|
}
|
|
5503
5894
|
transaction(callback) {
|
|
5504
|
-
return this.db.transaction(callback)();
|
|
5895
|
+
return this._m("transaction", () => this.db.transaction(callback)());
|
|
5505
5896
|
}
|
|
5506
5897
|
close() {
|
|
5507
5898
|
this._stopPolling();
|
|
5508
5899
|
this.db.close();
|
|
5509
5900
|
}
|
|
5510
5901
|
query(callback) {
|
|
5511
|
-
return executeProxyQuery(this.schemas, callback, (sql, params) => {
|
|
5512
|
-
if (this._debug)
|
|
5513
|
-
console.log("[satidb]", sql, params);
|
|
5902
|
+
return this._m("query(proxy)", () => executeProxyQuery(this.schemas, callback, (sql, params) => {
|
|
5514
5903
|
return this.db.query(sql).all(...params);
|
|
5515
|
-
});
|
|
5904
|
+
}));
|
|
5516
5905
|
}
|
|
5517
5906
|
raw(sql, ...params) {
|
|
5518
|
-
|
|
5519
|
-
console.log("[satidb]", sql, params);
|
|
5520
|
-
return this.db.query(sql).all(...params);
|
|
5907
|
+
return this._m(`raw: ${sql.slice(0, 60)}`, () => this.db.query(sql).all(...params));
|
|
5521
5908
|
}
|
|
5522
5909
|
exec(sql, ...params) {
|
|
5523
|
-
|
|
5524
|
-
console.log("[satidb]", sql, params);
|
|
5525
|
-
this.db.run(sql, ...params);
|
|
5910
|
+
this._m(`exec: ${sql.slice(0, 60)}`, () => this.db.run(sql, ...params));
|
|
5526
5911
|
}
|
|
5527
5912
|
tables() {
|
|
5528
5913
|
return Object.keys(this.schemas);
|
|
@@ -5531,70 +5916,76 @@ class _Database {
|
|
|
5531
5916
|
return this.db.query(`PRAGMA table_info("${tableName}")`).all();
|
|
5532
5917
|
}
|
|
5533
5918
|
dump() {
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5919
|
+
return this._m("dump", () => {
|
|
5920
|
+
const result = {};
|
|
5921
|
+
for (const tableName of Object.keys(this.schemas)) {
|
|
5922
|
+
result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
|
|
5923
|
+
}
|
|
5924
|
+
return result;
|
|
5925
|
+
});
|
|
5539
5926
|
}
|
|
5540
5927
|
load(data, options) {
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
const
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
const
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5928
|
+
this._m(`load(${Object.keys(data).join(",")})`, () => {
|
|
5929
|
+
const txn = this.db.transaction(() => {
|
|
5930
|
+
for (const [tableName, rows] of Object.entries(data)) {
|
|
5931
|
+
if (!this.schemas[tableName])
|
|
5932
|
+
continue;
|
|
5933
|
+
if (!options?.append) {
|
|
5934
|
+
this.db.run(`DELETE FROM "${tableName}"`);
|
|
5935
|
+
}
|
|
5936
|
+
for (const row of rows) {
|
|
5937
|
+
const cols = Object.keys(row).filter((k) => k !== "id");
|
|
5938
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
5939
|
+
const values = cols.map((c) => {
|
|
5940
|
+
const v = row[c];
|
|
5941
|
+
if (v !== null && v !== undefined && typeof v === "object" && !(v instanceof Buffer)) {
|
|
5942
|
+
return JSON.stringify(v);
|
|
5943
|
+
}
|
|
5944
|
+
return v;
|
|
5945
|
+
});
|
|
5946
|
+
this.db.query(`INSERT INTO "${tableName}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`).run(...values);
|
|
5947
|
+
}
|
|
5559
5948
|
}
|
|
5560
|
-
}
|
|
5949
|
+
});
|
|
5950
|
+
txn();
|
|
5561
5951
|
});
|
|
5562
|
-
txn();
|
|
5563
5952
|
}
|
|
5564
5953
|
seed(fixtures) {
|
|
5565
5954
|
this.load(fixtures, { append: true });
|
|
5566
5955
|
}
|
|
5567
5956
|
diff() {
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
const
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5957
|
+
return this._m("diff", () => {
|
|
5958
|
+
const result = {};
|
|
5959
|
+
const systemCols = new Set(["id", "createdAt", "updatedAt", "deletedAt"]);
|
|
5960
|
+
for (const [tableName, schema] of Object.entries(this.schemas)) {
|
|
5961
|
+
const schemaFields = getStorableFields(schema);
|
|
5962
|
+
const schemaColMap = new Map(schemaFields.map((f) => [f.name, zodTypeToSqlType(f.type)]));
|
|
5963
|
+
const liveColumns = this.columns(tableName);
|
|
5964
|
+
const liveColMap = new Map(liveColumns.map((c) => [c.name, c.type]));
|
|
5965
|
+
const added = [];
|
|
5966
|
+
const removed = [];
|
|
5967
|
+
const typeChanged = [];
|
|
5968
|
+
for (const [col, expectedType] of schemaColMap) {
|
|
5969
|
+
if (!liveColMap.has(col)) {
|
|
5970
|
+
added.push(col);
|
|
5971
|
+
} else {
|
|
5972
|
+
const actualType = liveColMap.get(col);
|
|
5973
|
+
if (actualType !== expectedType) {
|
|
5974
|
+
typeChanged.push({ column: col, expected: expectedType, actual: actualType });
|
|
5975
|
+
}
|
|
5585
5976
|
}
|
|
5586
5977
|
}
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5978
|
+
for (const col of liveColMap.keys()) {
|
|
5979
|
+
if (!systemCols.has(col) && !schemaColMap.has(col)) {
|
|
5980
|
+
removed.push(col);
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
|
|
5984
|
+
result[tableName] = { added, removed, typeChanged };
|
|
5591
5985
|
}
|
|
5592
5986
|
}
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
}
|
|
5596
|
-
}
|
|
5597
|
-
return result;
|
|
5987
|
+
return result;
|
|
5988
|
+
});
|
|
5598
5989
|
}
|
|
5599
5990
|
}
|
|
5600
5991
|
var Database = _Database;
|
package/package.json
CHANGED
|
@@ -1,57 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "sqlite-zod-orm",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Type-safe SQLite ORM for Bun — Zod schemas, fluent queries, auto relationships, zero SQL",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"module": "./dist/index.js",
|
|
8
|
-
"types": "./src/index.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"import": "./dist/index.js",
|
|
12
|
-
"types": "./src/index.ts"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm",
|
|
17
|
-
"clean": "rm -rf dist",
|
|
18
|
-
"test": "bun test",
|
|
19
|
-
"bench": "bun bench/triggers-vs-naive.ts && bun bench/poll-strategy.ts && bun bench/indexes.ts",
|
|
20
|
-
"prepublishOnly": "bun run build"
|
|
21
|
-
},
|
|
22
|
-
"files": [
|
|
23
|
-
"src",
|
|
24
|
-
"dist",
|
|
25
|
-
"README.md"
|
|
26
|
-
],
|
|
27
|
-
"keywords": [
|
|
28
|
-
"sqlite",
|
|
29
|
-
"database",
|
|
30
|
-
"bun",
|
|
31
|
-
"typescript",
|
|
32
|
-
"type-safe",
|
|
33
|
-
"orm",
|
|
34
|
-
"zod",
|
|
35
|
-
"sql",
|
|
36
|
-
"query-builder",
|
|
37
|
-
"relationships"
|
|
38
|
-
],
|
|
39
|
-
"author": "7flash",
|
|
40
|
-
"license": "MIT",
|
|
41
|
-
"repository": {
|
|
42
|
-
"type": "git",
|
|
43
|
-
"url": "git@github.com:7flash/sqlite-zod-orm.git"
|
|
44
|
-
},
|
|
45
|
-
"devDependencies": {
|
|
46
|
-
"bun-types": "latest"
|
|
47
|
-
},
|
|
48
|
-
"peerDependencies": {
|
|
49
|
-
"typescript": "^5.0.0"
|
|
50
|
-
},
|
|
51
|
-
"dependencies": {
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "sqlite-zod-orm",
|
|
3
|
+
"version": "3.25.0",
|
|
4
|
+
"description": "Type-safe SQLite ORM for Bun — Zod schemas, fluent queries, auto relationships, zero SQL",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm",
|
|
17
|
+
"clean": "rm -rf dist",
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"bench": "bun bench/triggers-vs-naive.ts && bun bench/poll-strategy.ts && bun bench/indexes.ts",
|
|
20
|
+
"prepublishOnly": "bun run build"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"sqlite",
|
|
29
|
+
"database",
|
|
30
|
+
"bun",
|
|
31
|
+
"typescript",
|
|
32
|
+
"type-safe",
|
|
33
|
+
"orm",
|
|
34
|
+
"zod",
|
|
35
|
+
"sql",
|
|
36
|
+
"query-builder",
|
|
37
|
+
"relationships"
|
|
38
|
+
],
|
|
39
|
+
"author": "7flash",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git@github.com:7flash/sqlite-zod-orm.git"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"bun-types": "latest"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"typescript": "^5.0.0"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"measure-fn": "^3.3.0",
|
|
53
|
+
"zod": "^3.25.67"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"bun": ">=1.0.0"
|
|
57
|
+
}
|
|
57
58
|
}
|
package/src/context.ts
CHANGED
|
@@ -40,4 +40,10 @@ export interface DatabaseContext {
|
|
|
40
40
|
|
|
41
41
|
/** Cascade delete config — parent table → list of child tables to auto-delete. */
|
|
42
42
|
cascade: Record<string, string[]>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Conditional measurement helper — wraps fn with measure-fn when debug is on.
|
|
46
|
+
* When debug is off, executes fn directly with zero overhead.
|
|
47
|
+
*/
|
|
48
|
+
_m<T>(label: string, fn: () => T): T;
|
|
43
49
|
}
|
package/src/crud.ts
CHANGED
|
@@ -66,9 +66,12 @@ export function insert<T extends Record<string, any>>(ctx: DatabaseContext, enti
|
|
|
66
66
|
? `INSERT INTO "${entityName}" DEFAULT VALUES`
|
|
67
67
|
: `INSERT INTO "${entityName}" (${quotedCols.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
let lastId = 0;
|
|
70
|
+
ctx._m(`SQL: ${sql.slice(0, 40)}`, () => {
|
|
71
|
+
const result = ctx.db.query(sql).run(...Object.values(transformed));
|
|
72
|
+
lastId = result.lastInsertRowid as number;
|
|
73
|
+
});
|
|
74
|
+
const newEntity = getById(ctx, entityName, lastId);
|
|
72
75
|
if (!newEntity) throw new Error('Failed to retrieve entity after insertion');
|
|
73
76
|
|
|
74
77
|
// afterInsert hook
|
|
@@ -99,8 +102,9 @@ export function update<T extends Record<string, any>>(ctx: DatabaseContext, enti
|
|
|
99
102
|
|
|
100
103
|
const setClause = Object.keys(transformed).map(key => `"${key}" = ?`).join(', ');
|
|
101
104
|
const sql = `UPDATE "${entityName}" SET ${setClause} WHERE id = ?`;
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
ctx._m(`SQL: UPDATE ${entityName} SET ...`, () => {
|
|
106
|
+
ctx.db.query(sql).run(...Object.values(transformed), id);
|
|
107
|
+
});
|
|
104
108
|
|
|
105
109
|
const updated = getById(ctx, entityName, id);
|
|
106
110
|
|
|
@@ -192,14 +196,12 @@ export function deleteWhere(ctx: DatabaseContext, entityName: string, conditions
|
|
|
192
196
|
// Soft delete: set deletedAt instead of removing rows
|
|
193
197
|
const now = new Date().toISOString();
|
|
194
198
|
const sql = `UPDATE "${entityName}" SET "deletedAt" = ? ${clause}`;
|
|
195
|
-
|
|
196
|
-
const result = ctx.db.query(sql).run(now, ...values);
|
|
199
|
+
const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx.db.query(sql).run(now, ...values));
|
|
197
200
|
return (result as any).changes ?? 0;
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
const sql = `DELETE FROM "${entityName}" ${clause}`;
|
|
201
|
-
|
|
202
|
-
const result = ctx.db.query(sql).run(...values);
|
|
204
|
+
const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx.db.query(sql).run(...values));
|
|
203
205
|
return (result as any).changes ?? 0;
|
|
204
206
|
}
|
|
205
207
|
|
package/src/database.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* focused modules.
|
|
7
7
|
*/
|
|
8
8
|
import { Database as SqliteDatabase } from 'bun:sqlite';
|
|
9
|
+
import { createMeasure } from 'measure-fn';
|
|
9
10
|
import { z } from 'zod';
|
|
10
11
|
import { QueryBuilder, executeProxyQuery, createQueryBuilder, type ProxyQueryResult } from './query';
|
|
11
12
|
import type {
|
|
@@ -63,7 +64,22 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
63
64
|
/** Poll interval in ms. */
|
|
64
65
|
private _pollInterval: number;
|
|
65
66
|
|
|
67
|
+
/** Scoped measure-fn instance for instrumentation. */
|
|
68
|
+
private _measure: ReturnType<typeof createMeasure>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Conditional measurement helper — wraps with measure-fn only when debug is on.
|
|
72
|
+
* When debug is off, executes fn directly with zero overhead.
|
|
73
|
+
*/
|
|
74
|
+
private _m<T>(label: string, fn: () => T): T {
|
|
75
|
+
if (this._debug) return this._measure.measureSync.assert(label, fn);
|
|
76
|
+
return fn();
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
constructor(dbFile: string, schemas: Schemas, options: DatabaseOptions = {}) {
|
|
80
|
+
this._debug = options.debug === true;
|
|
81
|
+
this._measure = createMeasure('satidb');
|
|
82
|
+
|
|
67
83
|
this.db = new SqliteDatabase(dbFile);
|
|
68
84
|
if (options.wal !== false) this.db.run('PRAGMA journal_mode = WAL');
|
|
69
85
|
this.db.run('PRAGMA foreign_keys = ON');
|
|
@@ -72,7 +88,6 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
72
88
|
this._reactive = options.reactive !== false; // default true
|
|
73
89
|
this._timestamps = options.timestamps === true;
|
|
74
90
|
this._softDeletes = options.softDeletes === true;
|
|
75
|
-
this._debug = options.debug === true;
|
|
76
91
|
this._pollInterval = options.pollInterval ?? 100;
|
|
77
92
|
this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
|
|
78
93
|
|
|
@@ -89,75 +104,78 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
89
104
|
hooks: options.hooks ?? {},
|
|
90
105
|
computed: options.computed ?? {},
|
|
91
106
|
cascade: options.cascade ?? {},
|
|
107
|
+
_m: <T>(label: string, fn: () => T): T => this._m(label, fn),
|
|
92
108
|
};
|
|
93
109
|
|
|
94
|
-
this.initializeTables();
|
|
95
|
-
if (this._reactive) this.initializeChangeTracking();
|
|
96
|
-
this.runMigrations();
|
|
97
|
-
if (options.indexes) this.createIndexes(options.indexes);
|
|
98
|
-
if (options.unique) this.createUniqueConstraints(options.unique);
|
|
110
|
+
this._m('Init tables', () => this.initializeTables());
|
|
111
|
+
if (this._reactive) this._m('Change tracking', () => this.initializeChangeTracking());
|
|
112
|
+
this._m('Run migrations', () => this.runMigrations());
|
|
113
|
+
if (options.indexes) this._m('Create indexes', () => this.createIndexes(options.indexes!));
|
|
114
|
+
if (options.unique) this._m('Unique constraints', () => this.createUniqueConstraints(options.unique!));
|
|
99
115
|
|
|
100
116
|
// Create typed entity accessors (db.users, db.posts, etc.)
|
|
101
117
|
for (const entityName of Object.keys(schemas)) {
|
|
102
118
|
const key = entityName as keyof Schemas;
|
|
103
119
|
const accessor: EntityAccessor<Schemas[typeof key]> = {
|
|
104
|
-
insert: (data) => insert(this._ctx, entityName, data),
|
|
105
|
-
insertMany: (rows: any[]) => insertMany(this._ctx, entityName, rows),
|
|
120
|
+
insert: (data) => this._m(`${entityName}.insert`, () => insert(this._ctx, entityName, data)),
|
|
121
|
+
insertMany: (rows: any[]) => this._m(`${entityName}.insertMany(${rows.length})`, () => insertMany(this._ctx, entityName, rows)),
|
|
106
122
|
update: (idOrData: any, data?: any) => {
|
|
107
|
-
if (typeof idOrData === 'number') return update(this._ctx, entityName, idOrData, data);
|
|
123
|
+
if (typeof idOrData === 'number') return this._m(`${entityName}.update(${idOrData})`, () => update(this._ctx, entityName, idOrData, data));
|
|
108
124
|
return createUpdateBuilder(this._ctx, entityName, idOrData);
|
|
109
125
|
},
|
|
110
|
-
upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
|
|
111
|
-
upsertMany: (rows: any[], conditions?: any) => upsertMany(this._ctx, entityName, rows, conditions),
|
|
112
|
-
findOrCreate: (conditions: any, defaults?: any) => findOrCreate(this._ctx, entityName, conditions, defaults),
|
|
126
|
+
upsert: (conditions, data) => this._m(`${entityName}.upsert`, () => upsert(this._ctx, entityName, data, conditions)),
|
|
127
|
+
upsertMany: (rows: any[], conditions?: any) => this._m(`${entityName}.upsertMany(${rows.length})`, () => upsertMany(this._ctx, entityName, rows, conditions)),
|
|
128
|
+
findOrCreate: (conditions: any, defaults?: any) => this._m(`${entityName}.findOrCreate`, () => findOrCreate(this._ctx, entityName, conditions, defaults)),
|
|
113
129
|
delete: ((id?: any) => {
|
|
114
130
|
if (typeof id === 'number') {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
131
|
+
return this._m(`${entityName}.delete(${id})`, () => {
|
|
132
|
+
// beforeDelete hook — return false to cancel
|
|
133
|
+
const hooks = this._ctx.hooks[entityName];
|
|
134
|
+
if (hooks?.beforeDelete) {
|
|
135
|
+
const result = hooks.beforeDelete(id);
|
|
136
|
+
if (result === false) return;
|
|
137
|
+
}
|
|
121
138
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
// Cascade delete children first
|
|
140
|
+
const cascadeTargets = this._ctx.cascade[entityName];
|
|
141
|
+
if (cascadeTargets) {
|
|
142
|
+
for (const childTable of cascadeTargets) {
|
|
143
|
+
const rel = this._ctx.relationships.find(
|
|
144
|
+
r => r.type === 'belongs-to' && r.from === childTable && r.to === entityName
|
|
145
|
+
);
|
|
146
|
+
if (rel) {
|
|
147
|
+
if (this._softDeletes) {
|
|
148
|
+
const now = new Date().toISOString();
|
|
149
|
+
this.db.query(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
|
|
150
|
+
} else {
|
|
151
|
+
this.db.query(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
|
|
152
|
+
}
|
|
136
153
|
}
|
|
137
154
|
}
|
|
138
155
|
}
|
|
139
|
-
}
|
|
140
156
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
if (this._softDeletes) {
|
|
158
|
+
const now = new Date().toISOString();
|
|
159
|
+
this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
|
|
160
|
+
if (hooks?.afterDelete) hooks.afterDelete(id);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
return deleteEntity(this._ctx, entityName, id);
|
|
164
|
+
});
|
|
148
165
|
}
|
|
149
166
|
return createDeleteBuilder(this._ctx, entityName);
|
|
150
167
|
}) as any,
|
|
151
168
|
restore: ((id: number) => {
|
|
152
169
|
if (!this._softDeletes) throw new Error('restore() requires softDeletes: true');
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
this._m(`${entityName}.restore(${id})`, () => {
|
|
171
|
+
this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
|
|
172
|
+
});
|
|
155
173
|
}) as any,
|
|
156
174
|
select: (...cols: string[]) => createQueryBuilder(this._ctx, entityName, cols),
|
|
157
|
-
count: () => {
|
|
175
|
+
count: () => this._m(`${entityName}.count`, () => {
|
|
158
176
|
const row = this.db.query(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ''}`).get() as any;
|
|
159
177
|
return row?.count ?? 0;
|
|
160
|
-
},
|
|
178
|
+
}),
|
|
161
179
|
on: (event: ChangeEvent, callback: (row: any) => void | Promise<void>) => {
|
|
162
180
|
return this._registerListener(entityName, event, callback);
|
|
163
181
|
},
|
|
@@ -364,7 +382,7 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
364
382
|
// =========================================================================
|
|
365
383
|
|
|
366
384
|
public transaction<T>(callback: () => T): T {
|
|
367
|
-
return this.db.transaction(callback)();
|
|
385
|
+
return this._m('transaction', () => this.db.transaction(callback)());
|
|
368
386
|
}
|
|
369
387
|
|
|
370
388
|
/** Close the database: stops polling and releases the SQLite handle. */
|
|
@@ -381,14 +399,13 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
381
399
|
public query<T extends Record<string, any> = Record<string, any>>(
|
|
382
400
|
callback: (ctx: { [K in keyof Schemas]: ProxyColumns<InferSchema<Schemas[K]>> }) => ProxyQueryResult
|
|
383
401
|
): T[] {
|
|
384
|
-
return executeProxyQuery(
|
|
402
|
+
return this._m('query(proxy)', () => executeProxyQuery(
|
|
385
403
|
this.schemas,
|
|
386
404
|
callback as any,
|
|
387
405
|
(sql: string, params: any[]) => {
|
|
388
|
-
if (this._debug) console.log('[satidb]', sql, params);
|
|
389
406
|
return this.db.query(sql).all(...params) as T[];
|
|
390
407
|
},
|
|
391
|
-
);
|
|
408
|
+
));
|
|
392
409
|
}
|
|
393
410
|
|
|
394
411
|
// =========================================================================
|
|
@@ -397,14 +414,12 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
397
414
|
|
|
398
415
|
/** Execute a raw SQL query and return results. */
|
|
399
416
|
public raw<T = any>(sql: string, ...params: any[]): T[] {
|
|
400
|
-
|
|
401
|
-
return this.db.query(sql).all(...params) as T[];
|
|
417
|
+
return this._m(`raw: ${sql.slice(0, 60)}`, () => this.db.query(sql).all(...params) as T[]);
|
|
402
418
|
}
|
|
403
419
|
|
|
404
420
|
/** Execute a raw SQL statement (INSERT/UPDATE/DELETE) without returning rows. */
|
|
405
421
|
public exec(sql: string, ...params: any[]): void {
|
|
406
|
-
|
|
407
|
-
this.db.run(sql, ...params);
|
|
422
|
+
this._m(`exec: ${sql.slice(0, 60)}`, () => this.db.run(sql, ...params));
|
|
408
423
|
}
|
|
409
424
|
|
|
410
425
|
// =========================================================================
|
|
@@ -430,11 +445,13 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
430
445
|
* Each key is a table name, value is an array of raw row objects.
|
|
431
446
|
*/
|
|
432
447
|
public dump(): Record<string, any[]> {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
448
|
+
return this._m('dump', () => {
|
|
449
|
+
const result: Record<string, any[]> = {};
|
|
450
|
+
for (const tableName of Object.keys(this.schemas)) {
|
|
451
|
+
result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
|
|
452
|
+
}
|
|
453
|
+
return result;
|
|
454
|
+
});
|
|
438
455
|
}
|
|
439
456
|
|
|
440
457
|
/**
|
|
@@ -442,28 +459,29 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
442
459
|
* Use `{ append: true }` to insert without truncating.
|
|
443
460
|
*/
|
|
444
461
|
public load(data: Record<string, any[]>, options?: { append?: boolean }): void {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
462
|
+
this._m(`load(${Object.keys(data).join(',')})`, () => {
|
|
463
|
+
const txn = this.db.transaction(() => {
|
|
464
|
+
for (const [tableName, rows] of Object.entries(data)) {
|
|
465
|
+
if (!this.schemas[tableName]) continue;
|
|
466
|
+
if (!options?.append) {
|
|
467
|
+
this.db.run(`DELETE FROM "${tableName}"`);
|
|
468
|
+
}
|
|
469
|
+
for (const row of rows) {
|
|
470
|
+
const cols = Object.keys(row).filter(k => k !== 'id');
|
|
471
|
+
const placeholders = cols.map(() => '?').join(', ');
|
|
472
|
+
const values = cols.map(c => {
|
|
473
|
+
const v = row[c];
|
|
474
|
+
if (v !== null && v !== undefined && typeof v === 'object' && !(v instanceof Buffer)) {
|
|
475
|
+
return JSON.stringify(v);
|
|
476
|
+
}
|
|
477
|
+
return v;
|
|
478
|
+
});
|
|
479
|
+
this.db.query(`INSERT INTO "${tableName}" (${cols.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`).run(...values);
|
|
480
|
+
}
|
|
463
481
|
}
|
|
464
|
-
}
|
|
482
|
+
});
|
|
483
|
+
txn();
|
|
465
484
|
});
|
|
466
|
-
txn();
|
|
467
485
|
}
|
|
468
486
|
|
|
469
487
|
/**
|
|
@@ -483,45 +501,45 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
483
501
|
* Returns a diff object per table: { added, removed, typeChanged }.
|
|
484
502
|
*/
|
|
485
503
|
public diff(): Record<string, { added: string[]; removed: string[]; typeChanged: { column: string; expected: string; actual: string }[] }> {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
504
|
+
return this._m('diff', () => {
|
|
505
|
+
const result: Record<string, { added: string[]; removed: string[]; typeChanged: { column: string; expected: string; actual: string }[] }> = {};
|
|
506
|
+
const systemCols = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt']);
|
|
507
|
+
|
|
508
|
+
for (const [tableName, schema] of Object.entries(this.schemas)) {
|
|
509
|
+
const schemaFields = getStorableFields(schema);
|
|
510
|
+
const schemaColMap = new Map(schemaFields.map(f => [f.name, zodTypeToSqlType(f.type)]));
|
|
511
|
+
|
|
512
|
+
const liveColumns = this.columns(tableName);
|
|
513
|
+
const liveColMap = new Map(liveColumns.map(c => [c.name, c.type]));
|
|
514
|
+
|
|
515
|
+
const added: string[] = [];
|
|
516
|
+
const removed: string[] = [];
|
|
517
|
+
const typeChanged: { column: string; expected: string; actual: string }[] = [];
|
|
518
|
+
|
|
519
|
+
for (const [col, expectedType] of schemaColMap) {
|
|
520
|
+
if (!liveColMap.has(col)) {
|
|
521
|
+
added.push(col);
|
|
522
|
+
} else {
|
|
523
|
+
const actualType = liveColMap.get(col)!;
|
|
524
|
+
if (actualType !== expectedType) {
|
|
525
|
+
typeChanged.push({ column: col, expected: expectedType, actual: actualType });
|
|
526
|
+
}
|
|
508
527
|
}
|
|
509
528
|
}
|
|
510
|
-
}
|
|
511
529
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
530
|
+
for (const col of liveColMap.keys()) {
|
|
531
|
+
if (!systemCols.has(col) && !schemaColMap.has(col)) {
|
|
532
|
+
removed.push(col);
|
|
533
|
+
}
|
|
516
534
|
}
|
|
517
|
-
}
|
|
518
535
|
|
|
519
|
-
|
|
520
|
-
|
|
536
|
+
if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
|
|
537
|
+
result[tableName] = { added, removed, typeChanged };
|
|
538
|
+
}
|
|
521
539
|
}
|
|
522
|
-
}
|
|
523
540
|
|
|
524
|
-
|
|
541
|
+
return result;
|
|
542
|
+
});
|
|
525
543
|
}
|
|
526
544
|
}
|
|
527
545
|
|
package/src/query.ts
CHANGED
|
@@ -39,10 +39,11 @@ export function createQueryBuilder(ctx: DatabaseContext, entityName: string, ini
|
|
|
39
39
|
const schema = ctx.schemas[entityName]!;
|
|
40
40
|
|
|
41
41
|
const executor = (sql: string, params: any[], raw: boolean): any[] => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
return ctx._m(`SQL: ${sql.slice(0, 60)}`, () => {
|
|
43
|
+
const rows = ctx.db.query(sql).all(...params);
|
|
44
|
+
if (raw) return rows;
|
|
45
|
+
return rows.map((row: any) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
|
|
46
|
+
});
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
const singleExecutor = (sql: string, params: any[], raw: boolean): any | null => {
|