sqlite-zod-orm 3.13.0 → 3.15.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
@@ -4042,6 +4042,8 @@ function zodTypeToSqlType(zodType) {
4042
4042
  return "TEXT";
4043
4043
  if (zodType instanceof exports_external.ZodNumber || zodType instanceof exports_external.ZodBoolean)
4044
4044
  return "INTEGER";
4045
+ if (zodType instanceof exports_external.ZodEnum)
4046
+ return "TEXT";
4045
4047
  if (zodType._def.typeName === "ZodInstanceOf" && zodType._def.type === Buffer)
4046
4048
  return "BLOB";
4047
4049
  return "TEXT";
@@ -4053,6 +4055,8 @@ function transformForStorage(data) {
4053
4055
  transformed[key] = value.toISOString();
4054
4056
  } else if (typeof value === "boolean") {
4055
4057
  transformed[key] = value ? 1 : 0;
4058
+ } else if (value !== null && value !== undefined && typeof value === "object" && !(value instanceof Buffer)) {
4059
+ transformed[key] = JSON.stringify(value);
4056
4060
  } else {
4057
4061
  transformed[key] = value;
4058
4062
  }
@@ -4073,12 +4077,23 @@ function transformFromStorage(row, schema) {
4073
4077
  transformed[key] = new Date(value);
4074
4078
  } else if (fieldSchema instanceof exports_external.ZodBoolean && typeof value === "number") {
4075
4079
  transformed[key] = value === 1;
4080
+ } else if (isJsonSchema(fieldSchema) && typeof value === "string") {
4081
+ try {
4082
+ transformed[key] = JSON.parse(value);
4083
+ } catch {
4084
+ transformed[key] = value;
4085
+ }
4076
4086
  } else {
4077
4087
  transformed[key] = value;
4078
4088
  }
4079
4089
  }
4080
4090
  return transformed;
4081
4091
  }
4092
+ function isJsonSchema(schema) {
4093
+ if (!schema)
4094
+ return false;
4095
+ return schema instanceof exports_external.ZodObject || schema instanceof exports_external.ZodArray || schema instanceof exports_external.ZodRecord || schema instanceof exports_external.ZodTuple || schema instanceof exports_external.ZodUnion || schema instanceof exports_external.ZodDiscriminatedUnion;
4096
+ }
4082
4097
 
4083
4098
  // src/ast.ts
4084
4099
  var wrapNode = (val) => val !== null && typeof val === "object" && ("type" in val) ? val : { type: "literal", value: val };
@@ -4244,6 +4259,12 @@ function compileIQO(tableName, iqo) {
4244
4259
  }
4245
4260
  }
4246
4261
  }
4262
+ if (iqo.rawWheres && iqo.rawWheres.length > 0) {
4263
+ for (const rw of iqo.rawWheres) {
4264
+ sql += sql.includes(" WHERE ") ? ` AND (${rw.sql})` : ` WHERE (${rw.sql})`;
4265
+ params.push(...rw.params);
4266
+ }
4267
+ }
4247
4268
  if (iqo.groupBy.length > 0) {
4248
4269
  sql += ` GROUP BY ${iqo.groupBy.join(", ")}`;
4249
4270
  }
@@ -4293,6 +4314,7 @@ class QueryBuilder {
4293
4314
  selects: [],
4294
4315
  wheres: [],
4295
4316
  whereOrs: [],
4317
+ rawWheres: [],
4296
4318
  whereAST: null,
4297
4319
  joins: [],
4298
4320
  groupBy: [],
@@ -4403,6 +4425,10 @@ class QueryBuilder {
4403
4425
  this.iqo.raw = true;
4404
4426
  return this;
4405
4427
  }
4428
+ whereRaw(sql, params = []) {
4429
+ this.iqo.rawWheres.push({ sql, params });
4430
+ return this;
4431
+ }
4406
4432
  with(...relations) {
4407
4433
  this.iqo.includes.push(...relations);
4408
4434
  return this;
@@ -5001,6 +5027,14 @@ function upsert(ctx, entityName, data, conditions = {}) {
5001
5027
  delete insertData.id;
5002
5028
  return insert(ctx, entityName, insertData);
5003
5029
  }
5030
+ function findOrCreate(ctx, entityName, conditions, defaults = {}) {
5031
+ const existing = getOne(ctx, entityName, conditions);
5032
+ if (existing)
5033
+ return { entity: existing, created: false };
5034
+ const data = { ...conditions, ...defaults };
5035
+ delete data.id;
5036
+ return { entity: insert(ctx, entityName, data), created: true };
5037
+ }
5004
5038
  function deleteEntity(ctx, entityName, id) {
5005
5039
  const hooks = ctx.hooks[entityName];
5006
5040
  if (hooks?.beforeDelete) {
@@ -5184,6 +5218,7 @@ class _Database {
5184
5218
  },
5185
5219
  upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
5186
5220
  upsertMany: (rows, conditions) => upsertMany(this._ctx, entityName, rows, conditions),
5221
+ findOrCreate: (conditions, defaults) => findOrCreate(this._ctx, entityName, conditions, defaults),
5187
5222
  delete: (id) => {
5188
5223
  if (typeof id === "number") {
5189
5224
  const hooks = this._ctx.hooks[entityName];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-zod-orm",
3
- "version": "3.13.0",
3
+ "version": "3.15.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[];
@@ -54,6 +54,7 @@ export class QueryBuilder<T extends Record<string, any>> {
54
54
  selects: [],
55
55
  wheres: [],
56
56
  whereOrs: [],
57
+ rawWheres: [],
57
58
  whereAST: null,
58
59
  joins: [],
59
60
  groupBy: [],
@@ -68,7 +69,9 @@ export class QueryBuilder<T extends Record<string, any>> {
68
69
  }
69
70
 
70
71
  /** Specify which columns to select. If called with no arguments, defaults to `*`. */
71
- 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 {
72
75
  this.iqo.selects.push(...cols);
73
76
  return this;
74
77
  }
@@ -215,6 +218,19 @@ export class QueryBuilder<T extends Record<string, any>> {
215
218
  return this;
216
219
  }
217
220
 
221
+ /**
222
+ * Add a raw SQL WHERE fragment with parameterized values.
223
+ * Can be combined with `.where()` — fragments are AND'd together.
224
+ *
225
+ * ```ts
226
+ * db.users.select().whereRaw('score > ? AND role != ?', [50, 'guest']).all()
227
+ * ```
228
+ */
229
+ whereRaw(sql: string, params: any[] = []): this {
230
+ this.iqo.rawWheres.push({ sql, params });
231
+ return this;
232
+ }
233
+
218
234
  /**
219
235
  * Eagerly load a related entity and attach as an array property.
220
236
  *
@@ -250,20 +266,20 @@ export class QueryBuilder<T extends Record<string, any>> {
250
266
  // ---------- Terminal / Execution Methods ----------
251
267
 
252
268
  /** Execute the query and return all matching rows. */
253
- all(): T[] {
269
+ all(): TResult[] {
254
270
  const { sql, params } = compileIQO(this.tableName, this.iqo);
255
271
  const results = this.executor(sql, params, this.iqo.raw);
256
- return this._applyEagerLoads(results);
272
+ return this._applyEagerLoads(results) as unknown as TResult[];
257
273
  }
258
274
 
259
275
  /** Execute the query and return the first matching row, or null. */
260
- get(): T | null {
276
+ get(): TResult | null {
261
277
  this.iqo.limit = 1;
262
278
  const { sql, params } = compileIQO(this.tableName, this.iqo);
263
279
  const result = this.singleExecutor(sql, params, this.iqo.raw);
264
280
  if (!result) return null;
265
281
  const [loaded] = this._applyEagerLoads([result]);
266
- return loaded ?? null;
282
+ return (loaded ?? null) as TResult | null;
267
283
  }
268
284
 
269
285
  /** Execute the query and return the count of matching rows. */
@@ -277,7 +293,7 @@ export class QueryBuilder<T extends Record<string, any>> {
277
293
  }
278
294
 
279
295
  /** Alias for get() — returns the first matching row or null. */
280
- first(): T | null {
296
+ first(): TResult | null {
281
297
  return this.get();
282
298
  }
283
299
 
@@ -385,7 +401,7 @@ export class QueryBuilder<T extends Record<string, any>> {
385
401
  }
386
402
 
387
403
  /** Paginate results. Returns { data, total, page, perPage, pages }. */
388
- 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 } {
389
405
  const total = this.count();
390
406
  const pages = Math.ceil(total / perPage);
391
407
  this.iqo.limit = perPage;
@@ -420,10 +436,10 @@ export class QueryBuilder<T extends Record<string, any>> {
420
436
 
421
437
  // ---------- Thenable (async/await support) ----------
422
438
 
423
- then<TResult1 = T[], TResult2 = never>(
424
- onfulfilled?: ((value: T[]) => TResult1 | PromiseLike<TResult1>) | null,
425
- onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
426
- ): 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> {
427
443
  try {
428
444
  const result = this.all();
429
445
  return Promise.resolve(result).then(onfulfilled, onrejected);
package/src/crud.ts CHANGED
@@ -156,6 +156,19 @@ export function upsert<T extends Record<string, any>>(ctx: DatabaseContext, enti
156
156
  return insert(ctx, entityName, insertData);
157
157
  }
158
158
 
159
+ /** Find a row matching conditions, or create it with the merged data. Returns { entity, created }. */
160
+ export function findOrCreate<T extends Record<string, any>>(
161
+ ctx: DatabaseContext, entityName: string,
162
+ conditions: Record<string, any>,
163
+ defaults: Record<string, any> = {},
164
+ ): { entity: AugmentedEntity<any>; created: boolean } {
165
+ const existing = getOne(ctx, entityName, conditions);
166
+ if (existing) return { entity: existing, created: false };
167
+ const data = { ...conditions, ...defaults };
168
+ delete (data as any).id;
169
+ return { entity: insert(ctx, entityName, data), created: true };
170
+ }
171
+
159
172
  export function deleteEntity(ctx: DatabaseContext, entityName: string, id: number): void {
160
173
  // beforeDelete hook — return false to cancel
161
174
  const hooks = ctx.hooks[entityName];
package/src/database.ts CHANGED
@@ -24,7 +24,7 @@ import type { DatabaseContext } from './context';
24
24
  import { buildWhereClause } from './helpers';
25
25
  import { attachMethods } from './entity';
26
26
  import {
27
- insert, insertMany, update, upsert, upsertMany, deleteEntity, createDeleteBuilder,
27
+ insert, insertMany, update, upsert, upsertMany, findOrCreate, deleteEntity, createDeleteBuilder,
28
28
  getById, getOne, findMany, updateWhere, createUpdateBuilder,
29
29
  } from './crud';
30
30
 
@@ -107,6 +107,7 @@ class _Database<Schemas extends SchemaMap> {
107
107
  },
108
108
  upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
109
109
  upsertMany: (rows: any[], conditions?: any) => upsertMany(this._ctx, entityName, rows, conditions),
110
+ findOrCreate: (conditions: any, defaults?: any) => findOrCreate(this._ctx, entityName, conditions, defaults),
110
111
  delete: ((id?: any) => {
111
112
  if (typeof id === 'number') {
112
113
  // beforeDelete hook — return false to cancel
package/src/iqo.ts CHANGED
@@ -31,6 +31,7 @@ export interface IQO {
31
31
  selects: string[];
32
32
  wheres: WhereCondition[];
33
33
  whereOrs: WhereCondition[][]; // Each sub-array is an OR group
34
+ rawWheres: { sql: string; params: any[] }[]; // Raw WHERE fragments
34
35
  whereAST: ASTNode | null;
35
36
  joins: JoinClause[];
36
37
  groupBy: string[];
@@ -162,6 +163,14 @@ export function compileIQO(tableName: string, iqo: IQO): { sql: string; params:
162
163
  }
163
164
  }
164
165
 
166
+ // Append raw WHERE fragments
167
+ if (iqo.rawWheres && iqo.rawWheres.length > 0) {
168
+ for (const rw of iqo.rawWheres) {
169
+ sql += sql.includes(' WHERE ') ? ` AND (${rw.sql})` : ` WHERE (${rw.sql})`;
170
+ params.push(...rw.params);
171
+ }
172
+ }
173
+
165
174
  // GROUP BY
166
175
  if (iqo.groupBy.length > 0) {
167
176
  sql += ` GROUP BY ${iqo.groupBy.join(', ')}`;
package/src/schema.ts CHANGED
@@ -80,7 +80,9 @@ export function zodTypeToSqlType(zodType: ZodType): string {
80
80
  }
81
81
  if (zodType instanceof z.ZodString || zodType instanceof z.ZodDate) return 'TEXT';
82
82
  if (zodType instanceof z.ZodNumber || zodType instanceof z.ZodBoolean) return 'INTEGER';
83
+ if (zodType instanceof z.ZodEnum) return 'TEXT';
83
84
  if ((zodType as any)._def.typeName === 'ZodInstanceOf' && (zodType as any)._def.type === Buffer) return 'BLOB';
85
+ // z.object(), z.array(), z.record() → stored as JSON TEXT
84
86
  return 'TEXT';
85
87
  }
86
88
 
@@ -92,6 +94,9 @@ export function transformForStorage(data: Record<string, any>): Record<string, a
92
94
  transformed[key] = value.toISOString();
93
95
  } else if (typeof value === 'boolean') {
94
96
  transformed[key] = value ? 1 : 0;
97
+ } else if (value !== null && value !== undefined && typeof value === 'object' && !(value instanceof Buffer)) {
98
+ // Auto-serialize objects and arrays to JSON
99
+ transformed[key] = JSON.stringify(value);
95
100
  } else {
96
101
  transformed[key] = value;
97
102
  }
@@ -114,9 +119,25 @@ export function transformFromStorage(row: Record<string, any>, schema: z.ZodType
114
119
  transformed[key] = new Date(value);
115
120
  } else if (fieldSchema instanceof z.ZodBoolean && typeof value === 'number') {
116
121
  transformed[key] = value === 1;
122
+ } else if (isJsonSchema(fieldSchema) && typeof value === 'string') {
123
+ // Auto-parse JSON columns
124
+ try { transformed[key] = JSON.parse(value); } catch { transformed[key] = value; }
117
125
  } else {
118
126
  transformed[key] = value;
119
127
  }
120
128
  }
121
129
  return transformed;
122
130
  }
131
+
132
+ /** Check if a Zod schema represents a JSON-serializable type */
133
+ function isJsonSchema(schema: any): boolean {
134
+ if (!schema) return false;
135
+ return (
136
+ schema instanceof z.ZodObject ||
137
+ schema instanceof z.ZodArray ||
138
+ schema instanceof z.ZodRecord ||
139
+ schema instanceof z.ZodTuple ||
140
+ schema instanceof z.ZodUnion ||
141
+ schema instanceof z.ZodDiscriminatedUnion
142
+ );
143
+ }
package/src/types.ts CHANGED
@@ -206,9 +206,13 @@ export type NavEntityAccessor<
206
206
  & ((data: Partial<Omit<z.input<S[Table & keyof S]>, 'id'>>) => UpdateBuilder<NavEntity<S, R, Table>>);
207
207
  upsert: (conditions?: Partial<z.infer<S[Table & keyof S]>>, data?: Partial<z.infer<S[Table & keyof S]>>) => NavEntity<S, R, Table>;
208
208
  upsertMany: (rows: Partial<z.infer<S[Table & keyof S]>>[], conditions?: Partial<z.infer<S[Table & keyof S]>>) => NavEntity<S, R, Table>[];
209
+ findOrCreate: (conditions: Partial<z.infer<S[Table & keyof S]>>, defaults?: Partial<z.infer<S[Table & keyof S]>>) => { entity: NavEntity<S, R, Table>; created: boolean };
209
210
  delete: ((id: number) => void) & (() => DeleteBuilder<NavEntity<S, R, Table>>);
210
211
  restore: (id: number) => void;
211
- select: (...cols: (keyof z.infer<S[Table & keyof S]> & string)[]) => QueryBuilder<NavEntity<S, R, Table>>;
212
+ select: {
213
+ (): QueryBuilder<NavEntity<S, R, Table>>;
214
+ <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
+ };
212
216
  on: ((event: 'insert' | 'update', callback: (row: NavEntity<S, R, Table>) => void | Promise<void>) => () => void) &
213
217
  ((event: 'delete', callback: (row: { id: number }) => void | Promise<void>) => () => void);
214
218
  _tableName: string;
@@ -238,10 +242,14 @@ export type EntityAccessor<S extends z.ZodType<any>> = {
238
242
  update: ((id: number, data: Partial<EntityData<S>>) => AugmentedEntity<S> | null) & ((data: Partial<EntityData<S>>) => UpdateBuilder<AugmentedEntity<S>>);
239
243
  upsert: (conditions?: Partial<InferSchema<S>>, data?: Partial<InferSchema<S>>) => AugmentedEntity<S>;
240
244
  upsertMany: (rows: Partial<InferSchema<S>>[], conditions?: Partial<InferSchema<S>>) => AugmentedEntity<S>[];
245
+ findOrCreate: (conditions: Partial<InferSchema<S>>, defaults?: Partial<InferSchema<S>>) => { entity: AugmentedEntity<S>; created: boolean };
241
246
  delete: ((id: number) => void) & (() => DeleteBuilder<AugmentedEntity<S>>);
242
247
  /** Undo a soft delete by setting deletedAt = null. Requires softDeletes. */
243
248
  restore: (id: number) => void;
244
- select: (...cols: (keyof InferSchema<S> & string)[]) => QueryBuilder<AugmentedEntity<S>>;
249
+ select: {
250
+ (): QueryBuilder<AugmentedEntity<S>>;
251
+ <K extends (keyof InferSchema<S> | 'id') & string>(...cols: K[]): QueryBuilder<AugmentedEntity<S>, Pick<AugmentedEntity<S>, K>>;
252
+ };
245
253
  on: ((event: 'insert' | 'update', callback: (row: AugmentedEntity<S>) => void | Promise<void>) => () => void) &
246
254
  ((event: 'delete', callback: (row: { id: number }) => void | Promise<void>) => () => void);
247
255
  _tableName: string;