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 +35 -0
- package/package.json +1 -1
- package/src/builder.ts +28 -12
- package/src/crud.ts +13 -0
- package/src/database.ts +2 -1
- package/src/iqo.ts +9 -0
- package/src/schema.ts +21 -0
- package/src/types.ts +10 -2
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
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(
|
|
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():
|
|
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():
|
|
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():
|
|
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:
|
|
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<
|
|
424
|
-
onfulfilled?: ((value:
|
|
425
|
-
onrejected?: ((reason: any) =>
|
|
426
|
-
): Promise<
|
|
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:
|
|
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:
|
|
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;
|