sqlite-zod-orm 3.15.0 → 3.17.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
  },
@@ -5419,6 +5449,40 @@ class _Database {
5419
5449
  columns(tableName) {
5420
5450
  return this.db.query(`PRAGMA table_info("${tableName}")`).all();
5421
5451
  }
5452
+ dump() {
5453
+ const result = {};
5454
+ for (const tableName of Object.keys(this.schemas)) {
5455
+ result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
5456
+ }
5457
+ return result;
5458
+ }
5459
+ load(data, options) {
5460
+ const txn = this.db.transaction(() => {
5461
+ for (const [tableName, rows] of Object.entries(data)) {
5462
+ if (!this.schemas[tableName])
5463
+ continue;
5464
+ if (!options?.append) {
5465
+ this.db.run(`DELETE FROM "${tableName}"`);
5466
+ }
5467
+ for (const row of rows) {
5468
+ const cols = Object.keys(row).filter((k) => k !== "id");
5469
+ const placeholders = cols.map(() => "?").join(", ");
5470
+ const values = cols.map((c) => {
5471
+ const v = row[c];
5472
+ if (v !== null && v !== undefined && typeof v === "object" && !(v instanceof Buffer)) {
5473
+ return JSON.stringify(v);
5474
+ }
5475
+ return v;
5476
+ });
5477
+ this.db.query(`INSERT INTO "${tableName}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`).run(...values);
5478
+ }
5479
+ }
5480
+ });
5481
+ txn();
5482
+ }
5483
+ seed(fixtures) {
5484
+ this.load(fixtures, { append: true });
5485
+ }
5422
5486
  }
5423
5487
  var Database = _Database;
5424
5488
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-zod-orm",
3
- "version": "3.15.0",
3
+ "version": "3.17.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/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
  },
@@ -395,6 +420,59 @@ class _Database<Schemas extends SchemaMap> {
395
420
  public columns(tableName: string): { name: string; type: string; notnull: number; pk: number }[] {
396
421
  return this.db.query(`PRAGMA table_info("${tableName}")`).all() as any[];
397
422
  }
423
+
424
+ // =========================================================================
425
+ // Data Import / Export
426
+ // =========================================================================
427
+
428
+ /**
429
+ * Export all data as a JSON-serializable object.
430
+ * Each key is a table name, value is an array of raw row objects.
431
+ */
432
+ public dump(): Record<string, any[]> {
433
+ const result: Record<string, any[]> = {};
434
+ for (const tableName of Object.keys(this.schemas)) {
435
+ result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
436
+ }
437
+ return result;
438
+ }
439
+
440
+ /**
441
+ * Import data from a dump object. Truncates existing data first.
442
+ * Use `{ append: true }` to insert without truncating.
443
+ */
444
+ public load(data: Record<string, any[]>, options?: { append?: boolean }): void {
445
+ const txn = this.db.transaction(() => {
446
+ for (const [tableName, rows] of Object.entries(data)) {
447
+ if (!this.schemas[tableName]) continue;
448
+ if (!options?.append) {
449
+ this.db.run(`DELETE FROM "${tableName}"`);
450
+ }
451
+ for (const row of rows) {
452
+ const cols = Object.keys(row).filter(k => k !== 'id');
453
+ const placeholders = cols.map(() => '?').join(', ');
454
+ const values = cols.map(c => {
455
+ const v = row[c];
456
+ // Auto-serialize objects/arrays
457
+ if (v !== null && v !== undefined && typeof v === 'object' && !(v instanceof Buffer)) {
458
+ return JSON.stringify(v);
459
+ }
460
+ return v;
461
+ });
462
+ this.db.query(`INSERT INTO "${tableName}" (${cols.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`).run(...values);
463
+ }
464
+ }
465
+ });
466
+ txn();
467
+ }
468
+
469
+ /**
470
+ * Seed tables with fixture data. Each key is a table name, value is an
471
+ * array of records to insert. Does NOT truncate — use for additive seeding.
472
+ */
473
+ public seed(fixtures: Record<string, Record<string, any>[]>): void {
474
+ this.load(fixtures, { append: true });
475
+ }
398
476
  }
399
477
 
400
478
  // =============================================================================
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 = {
@@ -213,6 +227,7 @@ export type NavEntityAccessor<
213
227
  (): QueryBuilder<NavEntity<S, R, Table>>;
214
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>>;
215
229
  };
230
+ count: () => number;
216
231
  on: ((event: 'insert' | 'update', callback: (row: NavEntity<S, R, Table>) => void | Promise<void>) => () => void) &
217
232
  ((event: 'delete', callback: (row: { id: number }) => void | Promise<void>) => () => void);
218
233
  _tableName: string;
@@ -250,6 +265,7 @@ export type EntityAccessor<S extends z.ZodType<any>> = {
250
265
  (): QueryBuilder<AugmentedEntity<S>>;
251
266
  <K extends (keyof InferSchema<S> | 'id') & string>(...cols: K[]): QueryBuilder<AugmentedEntity<S>, Pick<AugmentedEntity<S>, K>>;
252
267
  };
268
+ count: () => number;
253
269
  on: ((event: 'insert' | 'update', callback: (row: AugmentedEntity<S>) => void | Promise<void>) => () => void) &
254
270
  ((event: 'delete', callback: (row: { id: number }) => void | Promise<void>) => () => void);
255
271
  _tableName: string;