sqlite-zod-orm 3.13.0 → 3.14.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.14.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
@@ -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: [],
@@ -215,6 +216,19 @@ export class QueryBuilder<T extends Record<string, any>> {
215
216
  return this;
216
217
  }
217
218
 
219
+ /**
220
+ * Add a raw SQL WHERE fragment with parameterized values.
221
+ * Can be combined with `.where()` — fragments are AND'd together.
222
+ *
223
+ * ```ts
224
+ * db.users.select().whereRaw('score > ? AND role != ?', [50, 'guest']).all()
225
+ * ```
226
+ */
227
+ whereRaw(sql: string, params: any[] = []): this {
228
+ this.iqo.rawWheres.push({ sql, params });
229
+ return this;
230
+ }
231
+
218
232
  /**
219
233
  * Eagerly load a related entity and attach as an array property.
220
234
  *
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,6 +206,7 @@ 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
212
  select: (...cols: (keyof z.infer<S[Table & keyof S]> & string)[]) => QueryBuilder<NavEntity<S, R, Table>>;
@@ -238,6 +239,7 @@ export type EntityAccessor<S extends z.ZodType<any>> = {
238
239
  update: ((id: number, data: Partial<EntityData<S>>) => AugmentedEntity<S> | null) & ((data: Partial<EntityData<S>>) => UpdateBuilder<AugmentedEntity<S>>);
239
240
  upsert: (conditions?: Partial<InferSchema<S>>, data?: Partial<InferSchema<S>>) => AugmentedEntity<S>;
240
241
  upsertMany: (rows: Partial<InferSchema<S>>[], conditions?: Partial<InferSchema<S>>) => AugmentedEntity<S>[];
242
+ findOrCreate: (conditions: Partial<InferSchema<S>>, defaults?: Partial<InferSchema<S>>) => { entity: AugmentedEntity<S>; created: boolean };
241
243
  delete: ((id: number) => void) & (() => DeleteBuilder<AugmentedEntity<S>>);
242
244
  /** Undo a soft delete by setting deletedAt = null. Requires softDeletes. */
243
245
  restore: (id: number) => void;