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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-zod-orm",
3
- "version": "3.14.0",
3
+ "version": "3.16.0",
4
4
  "description": "Type-safe SQLite ORM for Bun — Zod schemas, fluent queries, auto relationships, zero SQL",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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(...cols: (keyof T & string)[]): this {
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(): T[] {
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(): T | null {
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(): T | null {
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: T[]; total: number; page: number; perPage: number; pages: number } {
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<TResult1 = T[], TResult2 = never>(
438
- onfulfilled?: ((value: T[]) => TResult1 | PromiseLike<TResult1>) | null,
439
- onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
440
- ): Promise<TResult1 | TResult2> {
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: (...cols: (keyof z.infer<S[Table & keyof S]> & string)[]) => QueryBuilder<NavEntity<S, R, Table>>;
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: (...cols: (keyof InferSchema<S> & string)[]) => QueryBuilder<AugmentedEntity<S>>;
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;