sqlite-zod-orm 3.14.0 → 3.16.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 +31 -1
- package/package.json +1 -1
- package/src/builder.ts +14 -12
- package/src/context.ts +6 -0
- package/src/database.ts +26 -1
- package/src/entity.ts +12 -0
- package/src/types.ts +24 -2
package/dist/index.js
CHANGED
|
@@ -5147,6 +5147,16 @@ function attachMethods(ctx, entityName, entity) {
|
|
|
5147
5147
|
}
|
|
5148
5148
|
}
|
|
5149
5149
|
}
|
|
5150
|
+
const computedGetters = ctx.computed[entityName];
|
|
5151
|
+
if (computedGetters) {
|
|
5152
|
+
for (const [key, fn] of Object.entries(computedGetters)) {
|
|
5153
|
+
Object.defineProperty(augmented, key, {
|
|
5154
|
+
get: () => fn(augmented),
|
|
5155
|
+
enumerable: true,
|
|
5156
|
+
configurable: true
|
|
5157
|
+
});
|
|
5158
|
+
}
|
|
5159
|
+
}
|
|
5150
5160
|
const storableFieldNames = new Set(getStorableFields(ctx.schemas[entityName]).map((f) => f.name));
|
|
5151
5161
|
return new Proxy(augmented, {
|
|
5152
5162
|
set: (target, prop, value) => {
|
|
@@ -5196,7 +5206,9 @@ class _Database {
|
|
|
5196
5206
|
debug: this._debug,
|
|
5197
5207
|
timestamps: this._timestamps,
|
|
5198
5208
|
softDeletes: this._softDeletes,
|
|
5199
|
-
hooks: options.hooks ?? {}
|
|
5209
|
+
hooks: options.hooks ?? {},
|
|
5210
|
+
computed: options.computed ?? {},
|
|
5211
|
+
cascade: options.cascade ?? {}
|
|
5200
5212
|
};
|
|
5201
5213
|
this.initializeTables();
|
|
5202
5214
|
if (this._reactive)
|
|
@@ -5227,6 +5239,20 @@ class _Database {
|
|
|
5227
5239
|
if (result === false)
|
|
5228
5240
|
return;
|
|
5229
5241
|
}
|
|
5242
|
+
const cascadeTargets = this._ctx.cascade[entityName];
|
|
5243
|
+
if (cascadeTargets) {
|
|
5244
|
+
for (const childTable of cascadeTargets) {
|
|
5245
|
+
const rel = this._ctx.relationships.find((r) => r.type === "belongs-to" && r.from === childTable && r.to === entityName);
|
|
5246
|
+
if (rel) {
|
|
5247
|
+
if (this._softDeletes) {
|
|
5248
|
+
const now = new Date().toISOString();
|
|
5249
|
+
this.db.query(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
|
|
5250
|
+
} else {
|
|
5251
|
+
this.db.query(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5230
5256
|
if (this._softDeletes) {
|
|
5231
5257
|
const now = new Date().toISOString();
|
|
5232
5258
|
this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
|
|
@@ -5246,6 +5272,10 @@ class _Database {
|
|
|
5246
5272
|
this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
|
|
5247
5273
|
},
|
|
5248
5274
|
select: (...cols) => createQueryBuilder(this._ctx, entityName, cols),
|
|
5275
|
+
count: () => {
|
|
5276
|
+
const row = this.db.query(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ""}`).get();
|
|
5277
|
+
return row?.count ?? 0;
|
|
5278
|
+
},
|
|
5249
5279
|
on: (event, callback) => {
|
|
5250
5280
|
return this._registerListener(entityName, event, callback);
|
|
5251
5281
|
},
|
package/package.json
CHANGED
package/src/builder.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
* - Object-style: `.where({ name: 'Alice', age: { $gt: 18 } })`
|
|
28
28
|
* - Callback-style (AST): `.where((c, f, op) => op.and(op.eq(c.name, 'Alice'), op.gt(c.age, 18)))`
|
|
29
29
|
*/
|
|
30
|
-
export class QueryBuilder<T extends Record<string, any
|
|
30
|
+
export class QueryBuilder<T extends Record<string, any>, TResult extends Record<string, any> = T> {
|
|
31
31
|
private iqo: IQO;
|
|
32
32
|
private tableName: string;
|
|
33
33
|
private executor: (sql: string, params: any[], raw: boolean) => any[];
|
|
@@ -69,7 +69,9 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/** Specify which columns to select. If called with no arguments, defaults to `*`. */
|
|
72
|
-
select(
|
|
72
|
+
select(): this;
|
|
73
|
+
select<K extends keyof T & string>(...cols: K[]): QueryBuilder<T, Pick<T, K>>;
|
|
74
|
+
select(...cols: string[]): any {
|
|
73
75
|
this.iqo.selects.push(...cols);
|
|
74
76
|
return this;
|
|
75
77
|
}
|
|
@@ -264,20 +266,20 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
264
266
|
// ---------- Terminal / Execution Methods ----------
|
|
265
267
|
|
|
266
268
|
/** Execute the query and return all matching rows. */
|
|
267
|
-
all():
|
|
269
|
+
all(): TResult[] {
|
|
268
270
|
const { sql, params } = compileIQO(this.tableName, this.iqo);
|
|
269
271
|
const results = this.executor(sql, params, this.iqo.raw);
|
|
270
|
-
return this._applyEagerLoads(results);
|
|
272
|
+
return this._applyEagerLoads(results) as unknown as TResult[];
|
|
271
273
|
}
|
|
272
274
|
|
|
273
275
|
/** Execute the query and return the first matching row, or null. */
|
|
274
|
-
get():
|
|
276
|
+
get(): TResult | null {
|
|
275
277
|
this.iqo.limit = 1;
|
|
276
278
|
const { sql, params } = compileIQO(this.tableName, this.iqo);
|
|
277
279
|
const result = this.singleExecutor(sql, params, this.iqo.raw);
|
|
278
280
|
if (!result) return null;
|
|
279
281
|
const [loaded] = this._applyEagerLoads([result]);
|
|
280
|
-
return loaded ?? null;
|
|
282
|
+
return (loaded ?? null) as TResult | null;
|
|
281
283
|
}
|
|
282
284
|
|
|
283
285
|
/** Execute the query and return the count of matching rows. */
|
|
@@ -291,7 +293,7 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
291
293
|
}
|
|
292
294
|
|
|
293
295
|
/** Alias for get() — returns the first matching row or null. */
|
|
294
|
-
first():
|
|
296
|
+
first(): TResult | null {
|
|
295
297
|
return this.get();
|
|
296
298
|
}
|
|
297
299
|
|
|
@@ -399,7 +401,7 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
399
401
|
}
|
|
400
402
|
|
|
401
403
|
/** Paginate results. Returns { data, total, page, perPage, pages }. */
|
|
402
|
-
paginate(page: number = 1, perPage: number = 20): { data:
|
|
404
|
+
paginate(page: number = 1, perPage: number = 20): { data: TResult[]; total: number; page: number; perPage: number; pages: number } {
|
|
403
405
|
const total = this.count();
|
|
404
406
|
const pages = Math.ceil(total / perPage);
|
|
405
407
|
this.iqo.limit = perPage;
|
|
@@ -434,10 +436,10 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
434
436
|
|
|
435
437
|
// ---------- Thenable (async/await support) ----------
|
|
436
438
|
|
|
437
|
-
then<
|
|
438
|
-
onfulfilled?: ((value:
|
|
439
|
-
onrejected?: ((reason: any) =>
|
|
440
|
-
): Promise<
|
|
439
|
+
then<TThen1 = TResult[], TThen2 = never>(
|
|
440
|
+
onfulfilled?: ((value: TResult[]) => TThen1 | PromiseLike<TThen1>) | null,
|
|
441
|
+
onrejected?: ((reason: any) => TThen2 | PromiseLike<TThen2>) | null,
|
|
442
|
+
): Promise<TThen1 | TThen2> {
|
|
441
443
|
try {
|
|
442
444
|
const result = this.all();
|
|
443
445
|
return Promise.resolve(result).then(onfulfilled, onrejected);
|
package/src/context.ts
CHANGED
|
@@ -34,4 +34,10 @@ export interface DatabaseContext {
|
|
|
34
34
|
|
|
35
35
|
/** Lifecycle hooks keyed by table name. */
|
|
36
36
|
hooks: Record<string, TableHooks>;
|
|
37
|
+
|
|
38
|
+
/** Computed/virtual getters per table. */
|
|
39
|
+
computed: Record<string, Record<string, (entity: Record<string, any>) => any>>;
|
|
40
|
+
|
|
41
|
+
/** Cascade delete config — parent table → list of child tables to auto-delete. */
|
|
42
|
+
cascade: Record<string, string[]>;
|
|
37
43
|
}
|
package/src/database.ts
CHANGED
|
@@ -87,6 +87,8 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
87
87
|
timestamps: this._timestamps,
|
|
88
88
|
softDeletes: this._softDeletes,
|
|
89
89
|
hooks: options.hooks ?? {},
|
|
90
|
+
computed: options.computed ?? {},
|
|
91
|
+
cascade: options.cascade ?? {},
|
|
90
92
|
};
|
|
91
93
|
|
|
92
94
|
this.initializeTables();
|
|
@@ -116,8 +118,27 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
116
118
|
const result = hooks.beforeDelete(id);
|
|
117
119
|
if (result === false) return;
|
|
118
120
|
}
|
|
121
|
+
|
|
122
|
+
// Cascade delete children first
|
|
123
|
+
const cascadeTargets = this._ctx.cascade[entityName];
|
|
124
|
+
if (cascadeTargets) {
|
|
125
|
+
for (const childTable of cascadeTargets) {
|
|
126
|
+
// Find FK from child → this parent via relationships
|
|
127
|
+
const rel = this._ctx.relationships.find(
|
|
128
|
+
r => r.type === 'belongs-to' && r.from === childTable && r.to === entityName
|
|
129
|
+
);
|
|
130
|
+
if (rel) {
|
|
131
|
+
if (this._softDeletes) {
|
|
132
|
+
const now = new Date().toISOString();
|
|
133
|
+
this.db.query(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
|
|
134
|
+
} else {
|
|
135
|
+
this.db.query(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
if (this._softDeletes) {
|
|
120
|
-
// Soft delete: set deletedAt instead of removing
|
|
121
142
|
const now = new Date().toISOString();
|
|
122
143
|
this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
|
|
123
144
|
if (hooks?.afterDelete) hooks.afterDelete(id);
|
|
@@ -133,6 +154,10 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
133
154
|
this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
|
|
134
155
|
}) as any,
|
|
135
156
|
select: (...cols: string[]) => createQueryBuilder(this._ctx, entityName, cols),
|
|
157
|
+
count: () => {
|
|
158
|
+
const row = this.db.query(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ''}`).get() as any;
|
|
159
|
+
return row?.count ?? 0;
|
|
160
|
+
},
|
|
136
161
|
on: (event: ChangeEvent, callback: (row: any) => void | Promise<void>) => {
|
|
137
162
|
return this._registerListener(entityName, event, callback);
|
|
138
163
|
},
|
package/src/entity.ts
CHANGED
|
@@ -47,6 +47,18 @@ export function attachMethods<T extends Record<string, any>>(
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Attach computed/virtual getters from context
|
|
51
|
+
const computedGetters = ctx.computed[entityName];
|
|
52
|
+
if (computedGetters) {
|
|
53
|
+
for (const [key, fn] of Object.entries(computedGetters)) {
|
|
54
|
+
Object.defineProperty(augmented, key, {
|
|
55
|
+
get: () => fn(augmented),
|
|
56
|
+
enumerable: true,
|
|
57
|
+
configurable: true,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
// Auto-persist proxy: setting a field auto-updates the DB row
|
|
51
63
|
const storableFieldNames = new Set(getStorableFields(ctx.schemas[entityName]!).map(f => f.name));
|
|
52
64
|
return new Proxy(augmented, {
|
package/src/types.ts
CHANGED
|
@@ -86,6 +86,20 @@ export type DatabaseOptions<R extends RelationsConfig = RelationsConfig> = {
|
|
|
86
86
|
* - `afterDelete(id)` — called after delete
|
|
87
87
|
*/
|
|
88
88
|
hooks?: Record<string, TableHooks>;
|
|
89
|
+
/**
|
|
90
|
+
* Computed/virtual getters per table. Injected on every read.
|
|
91
|
+
* ```ts
|
|
92
|
+
* computed: { users: { fullName: (u) => u.first + ' ' + u.last } }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
computed?: Record<string, Record<string, (entity: Record<string, any>) => any>>;
|
|
96
|
+
/**
|
|
97
|
+
* Cascade delete config per table. When a parent is deleted, children are auto-deleted.
|
|
98
|
+
* ```ts
|
|
99
|
+
* cascade: { authors: ['books'] } // deleting author → deletes their books
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
cascade?: Record<string, string[]>;
|
|
89
103
|
};
|
|
90
104
|
|
|
91
105
|
export type Relationship = {
|
|
@@ -209,7 +223,11 @@ export type NavEntityAccessor<
|
|
|
209
223
|
findOrCreate: (conditions: Partial<z.infer<S[Table & keyof S]>>, defaults?: Partial<z.infer<S[Table & keyof S]>>) => { entity: NavEntity<S, R, Table>; created: boolean };
|
|
210
224
|
delete: ((id: number) => void) & (() => DeleteBuilder<NavEntity<S, R, Table>>);
|
|
211
225
|
restore: (id: number) => void;
|
|
212
|
-
select:
|
|
226
|
+
select: {
|
|
227
|
+
(): QueryBuilder<NavEntity<S, R, Table>>;
|
|
228
|
+
<K extends (keyof z.infer<S[Table & keyof S]> | 'id') & string>(...cols: K[]): QueryBuilder<NavEntity<S, R, Table>, Pick<NavEntity<S, R, Table>, K>>;
|
|
229
|
+
};
|
|
230
|
+
count: () => number;
|
|
213
231
|
on: ((event: 'insert' | 'update', callback: (row: NavEntity<S, R, Table>) => void | Promise<void>) => () => void) &
|
|
214
232
|
((event: 'delete', callback: (row: { id: number }) => void | Promise<void>) => () => void);
|
|
215
233
|
_tableName: string;
|
|
@@ -243,7 +261,11 @@ export type EntityAccessor<S extends z.ZodType<any>> = {
|
|
|
243
261
|
delete: ((id: number) => void) & (() => DeleteBuilder<AugmentedEntity<S>>);
|
|
244
262
|
/** Undo a soft delete by setting deletedAt = null. Requires softDeletes. */
|
|
245
263
|
restore: (id: number) => void;
|
|
246
|
-
select:
|
|
264
|
+
select: {
|
|
265
|
+
(): QueryBuilder<AugmentedEntity<S>>;
|
|
266
|
+
<K extends (keyof InferSchema<S> | 'id') & string>(...cols: K[]): QueryBuilder<AugmentedEntity<S>, Pick<AugmentedEntity<S>, K>>;
|
|
267
|
+
};
|
|
268
|
+
count: () => number;
|
|
247
269
|
on: ((event: 'insert' | 'update', callback: (row: AugmentedEntity<S>) => void | Promise<void>) => () => void) &
|
|
248
270
|
((event: 'delete', callback: (row: { id: number }) => void | Promise<void>) => () => void);
|
|
249
271
|
_tableName: string;
|