sqlite-zod-orm 3.0.0 → 3.2.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/package.json CHANGED
@@ -1,20 +1,17 @@
1
1
  {
2
2
  "name": "sqlite-zod-orm",
3
- "version": "3.0.0",
4
- "description": "A fast, type-safe SQLite ORM for Bun powered by Zod schemas",
3
+ "version": "3.2.0",
4
+ "description": "Type-safe SQLite ORM for Bun Zod schemas, fluent queries, auto relationships, zero SQL",
5
5
  "type": "module",
6
- "main": "./dist/satidb.js",
7
- "module": "./dist/satidb.js",
8
- "types": "./src/satidb.ts",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./src/index.ts",
9
9
  "exports": {
10
10
  ".": {
11
- "import": "./dist/satidb.js",
12
- "types": "./src/satidb.ts"
11
+ "import": "./dist/index.js",
12
+ "types": "./src/index.ts"
13
13
  }
14
14
  },
15
- "bin": {
16
- "sqlite-zod-orm": "./dist/satidb.js"
17
- },
18
15
  "scripts": {
19
16
  "build": "bun run ./src/build.ts",
20
17
  "test": "bun test",
@@ -32,13 +29,16 @@
32
29
  "typescript",
33
30
  "type-safe",
34
31
  "orm",
35
- "sql"
32
+ "zod",
33
+ "sql",
34
+ "query-builder",
35
+ "relationships"
36
36
  ],
37
37
  "author": "7flash",
38
38
  "license": "MIT",
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "git@github.com:7flash/satidb.git"
41
+ "url": "git@github.com:7flash/sqlite-zod-orm.git"
42
42
  },
43
43
  "devDependencies": {
44
44
  "bun-types": "latest"
@@ -52,4 +52,4 @@
52
52
  "engines": {
53
53
  "bun": ">=1.0.0"
54
54
  }
55
- }
55
+ }
package/src/build.ts CHANGED
@@ -1,23 +1,21 @@
1
1
  import { EOL } from 'os';
2
2
 
3
- console.log("Starting build process for satidb...");
3
+ console.log("Building sqlite-zod-orm...");
4
4
 
5
5
  const result = await Bun.build({
6
- entrypoints: ['./src/satidb.ts'],
6
+ entrypoints: ['./src/index.ts'],
7
7
  outdir: './dist',
8
- target: 'bun', // Optimize for the Bun runtime
8
+ target: 'bun',
9
9
  format: 'esm',
10
- minify: true, // Minify for smaller file size
10
+ minify: false,
11
11
  });
12
12
 
13
13
  if (!result.success) {
14
- console.error("Build failed");
15
- for (const message of result.logs) {
16
- console.error(message);
14
+ console.error("Build failed:");
15
+ for (const msg of result.logs) {
16
+ console.error(msg);
17
17
  }
18
18
  process.exit(1);
19
19
  }
20
20
 
21
- const outFile = result.outputs[0].path;
22
-
23
- console.log(`Build successful! Executable created at: ${outFile}`);
21
+ console.log(`Build complete dist/ (${result.outputs.length} file${result.outputs.length > 1 ? 's' : ''})${EOL}`);
@@ -0,0 +1,491 @@
1
+ /**
2
+ * database.ts — Main Database class for sqlite-zod-orm
3
+ *
4
+ * Orchestrates schema-driven table creation, CRUD, relationships,
5
+ * query builders, and event handling.
6
+ */
7
+ import { Database as SqliteDatabase } from 'bun:sqlite';
8
+ import { EventEmitter } from 'events';
9
+ import { z } from 'zod';
10
+ import { QueryBuilder } from './query-builder';
11
+ import { executeProxyQuery, type ProxyQueryResult } from './proxy-query';
12
+ import type {
13
+ SchemaMap, DatabaseOptions, Relationship, RelationsConfig,
14
+ EntityAccessor, TypedAccessors, TypedNavAccessors, AugmentedEntity, UpdateBuilder,
15
+ ProxyColumns, InferSchema,
16
+ } from './types';
17
+ import { asZodObject } from './types';
18
+ import {
19
+ parseRelationsConfig,
20
+ getStorableFields,
21
+ zodTypeToSqlType, transformForStorage, transformFromStorage,
22
+ } from './schema';
23
+
24
+ // =============================================================================
25
+ // Database Class
26
+ // =============================================================================
27
+
28
+ class _Database<Schemas extends SchemaMap> extends EventEmitter {
29
+ private db: SqliteDatabase;
30
+ private schemas: Schemas;
31
+ private relationships: Relationship[];
32
+ private subscriptions: Record<'insert' | 'update' | 'delete', Record<string, ((data: any) => void)[]>>;
33
+ private options: DatabaseOptions;
34
+
35
+ constructor(dbFile: string, schemas: Schemas, options: DatabaseOptions = {}) {
36
+ super();
37
+ this.db = new SqliteDatabase(dbFile);
38
+ this.db.run('PRAGMA foreign_keys = ON');
39
+ this.schemas = schemas;
40
+ this.options = options;
41
+ this.subscriptions = { insert: {}, update: {}, delete: {} };
42
+ this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
43
+ this.initializeTables();
44
+ this.runMigrations();
45
+ if (options.indexes) this.createIndexes(options.indexes);
46
+ if (options.changeTracking) this.setupChangeTracking();
47
+
48
+ // Create typed entity accessors (db.users, db.posts, etc.)
49
+ for (const entityName of Object.keys(schemas)) {
50
+ const key = entityName as keyof Schemas;
51
+ const accessor: EntityAccessor<Schemas[typeof key]> = {
52
+ insert: (data) => this.insert(entityName, data),
53
+ update: (idOrData: any, data?: any) => {
54
+ if (typeof idOrData === 'number') return this.update(entityName, idOrData, data);
55
+ return this._createUpdateBuilder(entityName, idOrData);
56
+ },
57
+ upsert: (conditions, data) => this.upsert(entityName, data, conditions),
58
+ delete: (id) => this.delete(entityName, id),
59
+ subscribe: (event, callback) => this.subscribe(event, entityName, callback),
60
+ unsubscribe: (event, callback) => this.unsubscribe(event, entityName, callback),
61
+ select: (...cols: string[]) => this._createQueryBuilder(entityName, cols),
62
+ _tableName: entityName,
63
+ };
64
+ (this as any)[key] = accessor;
65
+ }
66
+ }
67
+
68
+ // ===========================================================================
69
+ // Table Initialization & Migrations
70
+ // ===========================================================================
71
+
72
+ private initializeTables(): void {
73
+ for (const [entityName, schema] of Object.entries(this.schemas)) {
74
+ const storableFields = getStorableFields(schema);
75
+ const columnDefs = storableFields.map(f => `${f.name} ${zodTypeToSqlType(f.type)}`);
76
+ const constraints: string[] = [];
77
+
78
+ // Add FOREIGN KEY constraints for FK columns declared in the schema
79
+ const belongsToRels = this.relationships.filter(
80
+ rel => rel.type === 'belongs-to' && rel.from === entityName
81
+ );
82
+ for (const rel of belongsToRels) {
83
+ constraints.push(`FOREIGN KEY (${rel.foreignKey}) REFERENCES ${rel.to}(id) ON DELETE SET NULL`);
84
+ }
85
+
86
+ const allCols = columnDefs.join(', ');
87
+ const allConstraints = constraints.length > 0 ? ', ' + constraints.join(', ') : '';
88
+ this.db.run(`CREATE TABLE IF NOT EXISTS ${entityName} (id INTEGER PRIMARY KEY AUTOINCREMENT, ${allCols}${allConstraints})`);
89
+ }
90
+ }
91
+
92
+ private runMigrations(): void {
93
+ this.db.run(`CREATE TABLE IF NOT EXISTS _schema_meta (
94
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
95
+ table_name TEXT NOT NULL,
96
+ column_name TEXT NOT NULL,
97
+ added_at TEXT DEFAULT (datetime('now')),
98
+ UNIQUE(table_name, column_name)
99
+ )`);
100
+
101
+ for (const [entityName, schema] of Object.entries(this.schemas)) {
102
+ const existingCols = new Set(
103
+ (this.db.query(`PRAGMA table_info(${entityName})`).all() as any[]).map(c => c.name)
104
+ );
105
+ const storableFields = getStorableFields(schema);
106
+
107
+ for (const field of storableFields) {
108
+ if (!existingCols.has(field.name)) {
109
+ this.db.run(`ALTER TABLE ${entityName} ADD COLUMN ${field.name} ${zodTypeToSqlType(field.type)}`);
110
+ this.db.query(`INSERT OR IGNORE INTO _schema_meta (table_name, column_name) VALUES (?, ?)`).run(entityName, field.name);
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ // ===========================================================================
117
+ // Indexes
118
+ // ===========================================================================
119
+
120
+ private createIndexes(indexDefs: Record<string, string | (string | string[])[]>): void {
121
+ for (const [tableName, indexes] of Object.entries(indexDefs)) {
122
+ if (!this.schemas[tableName]) throw new Error(`Cannot create index on unknown table '${tableName}'`);
123
+ const indexList = Array.isArray(indexes) ? indexes : [indexes];
124
+ for (const indexDef of indexList) {
125
+ const columns = Array.isArray(indexDef) ? indexDef : [indexDef];
126
+ const indexName = `idx_${tableName}_${columns.join('_')}`;
127
+ this.db.run(`CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName} (${columns.join(', ')})`);
128
+ }
129
+ }
130
+ }
131
+
132
+ // ===========================================================================
133
+ // Change Tracking
134
+ // ===========================================================================
135
+
136
+ private setupChangeTracking(): void {
137
+ this.db.run(`CREATE TABLE IF NOT EXISTS _changes (
138
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
139
+ table_name TEXT NOT NULL,
140
+ row_id INTEGER NOT NULL,
141
+ action TEXT NOT NULL CHECK(action IN ('INSERT', 'UPDATE', 'DELETE')),
142
+ changed_at TEXT DEFAULT (datetime('now'))
143
+ )`);
144
+ this.db.run(`CREATE INDEX IF NOT EXISTS idx_changes_table ON _changes (table_name, id)`);
145
+
146
+ for (const entityName of Object.keys(this.schemas)) {
147
+ for (const action of ['insert', 'update', 'delete'] as const) {
148
+ const ref = action === 'delete' ? 'OLD' : 'NEW';
149
+ this.db.run(`CREATE TRIGGER IF NOT EXISTS _trg_${entityName}_${action}
150
+ AFTER ${action.toUpperCase()} ON ${entityName}
151
+ BEGIN
152
+ INSERT INTO _changes (table_name, row_id, action) VALUES ('${entityName}', ${ref}.id, '${action.toUpperCase()}');
153
+ END`);
154
+ }
155
+ }
156
+ }
157
+
158
+ public getChangeSeq(tableName?: string): number {
159
+ if (!this.options.changeTracking) return -1;
160
+ const sql = tableName
161
+ ? `SELECT MAX(id) as seq FROM _changes WHERE table_name = ?`
162
+ : `SELECT MAX(id) as seq FROM _changes`;
163
+ const row = this.db.query(sql).get(...(tableName ? [tableName] : [])) as any;
164
+ return row?.seq ?? 0;
165
+ }
166
+
167
+ public getChangesSince(sinceSeq: number, tableName?: string) {
168
+ const sql = tableName
169
+ ? `SELECT * FROM _changes WHERE id > ? AND table_name = ? ORDER BY id ASC`
170
+ : `SELECT * FROM _changes WHERE id > ? ORDER BY id ASC`;
171
+ return this.db.query(sql).all(...(tableName ? [sinceSeq, tableName] : [sinceSeq])) as any[];
172
+ }
173
+
174
+ // ===========================================================================
175
+ // CRUD
176
+ // ===========================================================================
177
+
178
+ private insert<T extends Record<string, any>>(entityName: string, data: Omit<T, 'id'>): AugmentedEntity<any> {
179
+ const schema = this.schemas[entityName]!;
180
+ const validatedData = asZodObject(schema).passthrough().parse(data);
181
+ const transformed = transformForStorage(validatedData);
182
+ const columns = Object.keys(transformed);
183
+
184
+ const sql = columns.length === 0
185
+ ? `INSERT INTO ${entityName} DEFAULT VALUES`
186
+ : `INSERT INTO ${entityName} (${columns.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
187
+
188
+ const result = this.db.query(sql).run(...Object.values(transformed));
189
+ const newEntity = this._getById(entityName, result.lastInsertRowid as number);
190
+ if (!newEntity) throw new Error('Failed to retrieve entity after insertion');
191
+
192
+ this.emit('insert', entityName, newEntity);
193
+ this.subscriptions.insert[entityName]?.forEach(cb => cb(newEntity));
194
+ return newEntity;
195
+ }
196
+
197
+ /** Internal: get a single entity by ID */
198
+ private _getById(entityName: string, id: number): AugmentedEntity<any> | null {
199
+ const row = this.db.query(`SELECT * FROM ${entityName} WHERE id = ?`).get(id) as any;
200
+ if (!row) return null;
201
+ return this._attachMethods(entityName, transformFromStorage(row, this.schemas[entityName]!));
202
+ }
203
+
204
+ /** Internal: get a single entity by conditions */
205
+ private _getOne(entityName: string, conditions: Record<string, any>): AugmentedEntity<any> | null {
206
+ const { clause, values } = this.buildWhereClause(conditions);
207
+ const row = this.db.query(`SELECT * FROM ${entityName} ${clause} LIMIT 1`).get(...values) as any;
208
+ if (!row) return null;
209
+ return this._attachMethods(entityName, transformFromStorage(row, this.schemas[entityName]!));
210
+ }
211
+
212
+ /** Internal: find multiple entities by conditions */
213
+ private _findMany(entityName: string, conditions: Record<string, any> = {}): AugmentedEntity<any>[] {
214
+ const { clause, values } = this.buildWhereClause(conditions);
215
+ const rows = this.db.query(`SELECT * FROM ${entityName} ${clause}`).all(...values);
216
+ return rows.map((row: any) =>
217
+ this._attachMethods(entityName, transformFromStorage(row, this.schemas[entityName]!))
218
+ );
219
+ }
220
+
221
+ private update<T extends Record<string, any>>(entityName: string, id: number, data: Partial<Omit<T, 'id'>>): AugmentedEntity<any> | null {
222
+ const schema = this.schemas[entityName]!;
223
+ const validatedData = asZodObject(schema).partial().parse(data);
224
+ const transformed = transformForStorage(validatedData);
225
+ if (Object.keys(transformed).length === 0) return this._getById(entityName, id);
226
+
227
+ const setClause = Object.keys(transformed).map(key => `${key} = ?`).join(', ');
228
+ this.db.query(`UPDATE ${entityName} SET ${setClause} WHERE id = ?`).run(...Object.values(transformed), id);
229
+
230
+ const updatedEntity = this._getById(entityName, id);
231
+ if (updatedEntity) {
232
+ this.emit('update', entityName, updatedEntity);
233
+ this.subscriptions.update[entityName]?.forEach(cb => cb(updatedEntity));
234
+ }
235
+ return updatedEntity;
236
+ }
237
+
238
+ private _updateWhere(entityName: string, data: Record<string, any>, conditions: Record<string, any>): number {
239
+ const schema = this.schemas[entityName]!;
240
+ const validatedData = asZodObject(schema).partial().parse(data);
241
+ const transformed = transformForStorage(validatedData);
242
+ if (Object.keys(transformed).length === 0) return 0;
243
+
244
+ const { clause, values: whereValues } = this.buildWhereClause(conditions);
245
+ if (!clause) throw new Error('update().where() requires at least one condition');
246
+
247
+ const setCols = Object.keys(transformed);
248
+ const setClause = setCols.map(key => `${key} = ?`).join(', ');
249
+ const result = this.db.query(`UPDATE ${entityName} SET ${setClause} ${clause}`).run(
250
+ ...setCols.map(key => transformed[key]),
251
+ ...whereValues
252
+ );
253
+
254
+ const affected = (result as any).changes ?? 0;
255
+ if (affected > 0 && (this.subscriptions.update[entityName]?.length || this.options.changeTracking)) {
256
+ for (const entity of this._findMany(entityName, conditions)) {
257
+ this.emit('update', entityName, entity);
258
+ this.subscriptions.update[entityName]?.forEach(cb => cb(entity));
259
+ }
260
+ }
261
+ return affected;
262
+ }
263
+
264
+ private _createUpdateBuilder(entityName: string, data: Record<string, any>): UpdateBuilder<any> {
265
+ let _conditions: Record<string, any> = {};
266
+ const builder: UpdateBuilder<any> = {
267
+ where: (conditions) => { _conditions = { ..._conditions, ...conditions }; return builder; },
268
+ exec: () => this._updateWhere(entityName, data, _conditions),
269
+ };
270
+ return builder;
271
+ }
272
+
273
+ private upsert<T extends Record<string, any>>(entityName: string, data: any, conditions: any = {}): AugmentedEntity<any> {
274
+ const hasId = data?.id && typeof data.id === 'number';
275
+ const existing = hasId
276
+ ? this._getById(entityName, data.id)
277
+ : Object.keys(conditions ?? {}).length > 0
278
+ ? this._getOne(entityName, conditions)
279
+ : null;
280
+
281
+ if (existing) {
282
+ const updateData = { ...data };
283
+ delete updateData.id;
284
+ return this.update(entityName, existing.id, updateData) as AugmentedEntity<any>;
285
+ }
286
+ const insertData = { ...(conditions ?? {}), ...(data ?? {}) };
287
+ delete insertData.id;
288
+ return this.insert(entityName, insertData);
289
+ }
290
+
291
+ private delete(entityName: string, id: number): void {
292
+ const entity = this._getById(entityName, id);
293
+ if (entity) {
294
+ this.db.query(`DELETE FROM ${entityName} WHERE id = ?`).run(id);
295
+ this.emit('delete', entityName, entity);
296
+ this.subscriptions.delete[entityName]?.forEach(cb => cb(entity));
297
+ }
298
+ }
299
+
300
+ // ===========================================================================
301
+ // Entity Methods
302
+ // ===========================================================================
303
+
304
+ private _attachMethods<T extends Record<string, any>>(
305
+ entityName: string, entity: T
306
+ ): AugmentedEntity<any> {
307
+ const augmented = entity as any;
308
+ augmented.update = (data: any) => this.update(entityName, entity.id, data);
309
+ augmented.delete = () => this.delete(entityName, entity.id);
310
+
311
+ // Attach lazy relationship navigation
312
+ for (const rel of this.relationships) {
313
+ if (rel.from === entityName && rel.type === 'belongs-to') {
314
+ // book.author() → lazy load parent via author_id FK
315
+ augmented[rel.relationshipField] = () => {
316
+ const fkValue = entity[rel.foreignKey];
317
+ return fkValue ? this._getById(rel.to, fkValue) : null;
318
+ };
319
+ } else if (rel.from === entityName && rel.type === 'one-to-many') {
320
+ // author.books() → lazy load children
321
+ const belongsToRel = this.relationships.find(
322
+ r => r.type === 'belongs-to' && r.from === rel.to && r.to === rel.from
323
+ );
324
+ if (belongsToRel) {
325
+ const fk = belongsToRel.foreignKey;
326
+ augmented[rel.relationshipField] = () => {
327
+ return this._findMany(rel.to, { [fk]: entity.id });
328
+ };
329
+ }
330
+ }
331
+ }
332
+
333
+ // Auto-persist proxy: setting a field auto-updates the DB row
334
+ const storableFieldNames = new Set(getStorableFields(this.schemas[entityName]!).map(f => f.name));
335
+ return new Proxy(augmented, {
336
+ set: (target, prop: string, value) => {
337
+ if (storableFieldNames.has(prop) && target[prop] !== value) {
338
+ this.update(entityName, target.id, { [prop]: value });
339
+ }
340
+ target[prop] = value;
341
+ return true;
342
+ },
343
+ get: (target, prop, receiver) => Reflect.get(target, prop, receiver),
344
+ });
345
+ }
346
+
347
+ // ===========================================================================
348
+ // SQL Helpers
349
+ // ===========================================================================
350
+
351
+ private buildWhereClause(conditions: Record<string, any>, tablePrefix?: string): { clause: string; values: any[] } {
352
+ const parts: string[] = [];
353
+ const values: any[] = [];
354
+
355
+ for (const key in conditions) {
356
+ if (key.startsWith('$')) {
357
+ if (key === '$or' && Array.isArray(conditions[key])) {
358
+ const orBranches = conditions[key] as Record<string, any>[];
359
+ const orParts: string[] = [];
360
+ for (const branch of orBranches) {
361
+ const sub = this.buildWhereClause(branch, tablePrefix);
362
+ if (sub.clause) {
363
+ orParts.push(`(${sub.clause.replace(/^WHERE /, '')})`);
364
+ values.push(...sub.values);
365
+ }
366
+ }
367
+ if (orParts.length > 0) parts.push(`(${orParts.join(' OR ')})`);
368
+ }
369
+ continue;
370
+ }
371
+ const value = conditions[key];
372
+ const fieldName = tablePrefix ? `${tablePrefix}.${key}` : key;
373
+
374
+ if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
375
+ const operator = Object.keys(value)[0];
376
+ if (!operator?.startsWith('$')) {
377
+ throw new Error(`Querying on nested object '${key}' not supported. Use operators like $gt.`);
378
+ }
379
+ const operand = value[operator];
380
+
381
+ if (operator === '$in') {
382
+ if (!Array.isArray(operand)) throw new Error(`$in for '${key}' requires an array`);
383
+ if (operand.length === 0) { parts.push('1 = 0'); continue; }
384
+ parts.push(`${fieldName} IN (${operand.map(() => '?').join(', ')})`);
385
+ values.push(...operand.map((v: any) => transformForStorage({ v }).v));
386
+ continue;
387
+ }
388
+
389
+ const sqlOp = ({ $gt: '>', $gte: '>=', $lt: '<', $lte: '<=', $ne: '!=' } as Record<string, string>)[operator];
390
+ if (!sqlOp) throw new Error(`Unsupported operator '${operator}' on '${key}'`);
391
+ parts.push(`${fieldName} ${sqlOp} ?`);
392
+ values.push(transformForStorage({ operand }).operand);
393
+ } else {
394
+ parts.push(`${fieldName} = ?`);
395
+ values.push(transformForStorage({ value }).value);
396
+ }
397
+ }
398
+
399
+ return { clause: parts.length > 0 ? `WHERE ${parts.join(' AND ')}` : '', values };
400
+ }
401
+
402
+ // ===========================================================================
403
+ // Transactions
404
+ // ===========================================================================
405
+
406
+ public transaction<T>(callback: () => T): T {
407
+ try {
408
+ this.db.run('BEGIN TRANSACTION');
409
+ const result = callback();
410
+ this.db.run('COMMIT');
411
+ return result;
412
+ } catch (error) {
413
+ this.db.run('ROLLBACK');
414
+ throw new Error(`Transaction failed: ${(error as Error).message}`);
415
+ }
416
+ }
417
+
418
+ // ===========================================================================
419
+ // Events
420
+ // ===========================================================================
421
+
422
+ private subscribe(event: 'insert' | 'update' | 'delete', entityName: string, callback: (data: any) => void): void {
423
+ this.subscriptions[event][entityName] = this.subscriptions[event][entityName] || [];
424
+ this.subscriptions[event][entityName].push(callback);
425
+ }
426
+
427
+ private unsubscribe(event: 'insert' | 'update' | 'delete', entityName: string, callback: (data: any) => void): void {
428
+ if (this.subscriptions[event][entityName]) {
429
+ this.subscriptions[event][entityName] = this.subscriptions[event][entityName].filter(cb => cb !== callback);
430
+ }
431
+ }
432
+
433
+ // ===========================================================================
434
+ // Query Builders
435
+ // ===========================================================================
436
+
437
+ private _createQueryBuilder(entityName: string, initialCols: string[]): QueryBuilder<any> {
438
+ const schema = this.schemas[entityName]!;
439
+
440
+ const executor = (sql: string, params: any[], raw: boolean): any[] => {
441
+ const rows = this.db.query(sql).all(...params);
442
+ if (raw) return rows;
443
+ return rows.map((row: any) => this._attachMethods(entityName, transformFromStorage(row, schema)));
444
+ };
445
+
446
+ const singleExecutor = (sql: string, params: any[], raw: boolean): any | null => {
447
+ const results = executor(sql, params, raw);
448
+ return results.length > 0 ? results[0] : null;
449
+ };
450
+
451
+ const joinResolver = (fromTable: string, toTable: string): { fk: string; pk: string } | null => {
452
+ const belongsTo = this.relationships.find(
453
+ r => r.type === 'belongs-to' && r.from === fromTable && r.to === toTable
454
+ );
455
+ if (belongsTo) return { fk: belongsTo.foreignKey, pk: 'id' };
456
+ const reverse = this.relationships.find(
457
+ r => r.type === 'belongs-to' && r.from === toTable && r.to === fromTable
458
+ );
459
+ if (reverse) return { fk: 'id', pk: reverse.foreignKey };
460
+ return null;
461
+ };
462
+
463
+ const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver);
464
+ if (initialCols.length > 0) builder.select(...initialCols);
465
+ return builder;
466
+ }
467
+
468
+ /** Proxy callback query for complex SQL-like JOINs */
469
+ public query<T extends Record<string, any> = Record<string, any>>(
470
+ callback: (ctx: { [K in keyof Schemas]: ProxyColumns<InferSchema<Schemas[K]>> }) => ProxyQueryResult
471
+ ): T[] {
472
+ return executeProxyQuery(
473
+ this.schemas,
474
+ callback as any,
475
+ (sql: string, params: any[]) => this.db.query(sql).all(...params) as T[],
476
+ );
477
+ }
478
+ }
479
+
480
+ // =============================================================================
481
+ // Public Export
482
+ // =============================================================================
483
+
484
+ const Database = _Database as unknown as new <S extends SchemaMap, const R extends RelationsConfig = {}>(
485
+ dbFile: string, schemas: S, options?: DatabaseOptions<R>
486
+ ) => _Database<S> & TypedNavAccessors<S, R>;
487
+
488
+ type Database<S extends SchemaMap, R extends RelationsConfig = {}> = _Database<S> & TypedNavAccessors<S, R>;
489
+
490
+ export { Database };
491
+ export type { Database as DatabaseType };
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * sqlite-zod-orm — Type-safe SQLite ORM for Bun with Zod schemas.
3
+ *
4
+ * @module sqlite-zod-orm
5
+ */
6
+ export { Database } from './database';
7
+ export type { DatabaseType } from './database';
8
+
9
+ export type {
10
+ SchemaMap, DatabaseOptions, Relationship,
11
+ EntityAccessor, TypedAccessors, AugmentedEntity, UpdateBuilder,
12
+ InferSchema, EntityData, IndexDef,
13
+ ProxyColumns, ColumnRef,
14
+ } from './types';
15
+
16
+ export { z } from 'zod';
17
+
18
+ export { QueryBuilder } from './query-builder';
19
+ export { ColumnNode, type ProxyQueryResult } from './proxy-query';
20
+ export {
21
+ type ASTNode, type WhereCallback, type SetCallback,
22
+ type TypedColumnProxy, type FunctionProxy, type Operators,
23
+ compileAST, wrapNode, createColumnProxy, createFunctionProxy, op,
24
+ } from './ast';