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 +65 -1
- package/package.json +1 -1
- package/src/context.ts +6 -0
- package/src/database.ts +79 -1
- package/src/entity.ts +12 -0
- package/src/types.ts +16 -0
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
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;
|