sqlite-zod-orm 3.9.0 → 3.10.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 +89 -65
- package/package.json +1 -1
- package/src/builder.ts +311 -0
- package/src/crud.ts +19 -1
- package/src/database.ts +5 -2
- package/src/iqo.ts +172 -0
- package/src/proxy.ts +276 -0
- package/src/query.ts +16 -736
- package/src/types.ts +8 -2
package/dist/index.js
CHANGED
|
@@ -13,65 +13,6 @@ var __export = (target, all) => {
|
|
|
13
13
|
// src/database.ts
|
|
14
14
|
import { Database as SqliteDatabase } from "bun:sqlite";
|
|
15
15
|
|
|
16
|
-
// src/ast.ts
|
|
17
|
-
var wrapNode = (val) => val !== null && typeof val === "object" && ("type" in val) ? val : { type: "literal", value: val };
|
|
18
|
-
function compileAST(node) {
|
|
19
|
-
if (node.type === "column")
|
|
20
|
-
return { sql: `"${node.name}"`, params: [] };
|
|
21
|
-
if (node.type === "literal") {
|
|
22
|
-
if (node.value instanceof Date)
|
|
23
|
-
return { sql: "?", params: [node.value.toISOString()] };
|
|
24
|
-
if (typeof node.value === "boolean")
|
|
25
|
-
return { sql: "?", params: [node.value ? 1 : 0] };
|
|
26
|
-
return { sql: "?", params: [node.value] };
|
|
27
|
-
}
|
|
28
|
-
if (node.type === "function") {
|
|
29
|
-
const compiledArgs = node.args.map(compileAST);
|
|
30
|
-
return {
|
|
31
|
-
sql: `${node.name}(${compiledArgs.map((c) => c.sql).join(", ")})`,
|
|
32
|
-
params: compiledArgs.flatMap((c) => c.params)
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
if (node.type === "operator") {
|
|
36
|
-
const left = compileAST(node.left);
|
|
37
|
-
const right = compileAST(node.right);
|
|
38
|
-
return {
|
|
39
|
-
sql: `(${left.sql} ${node.op} ${right.sql})`,
|
|
40
|
-
params: [...left.params, ...right.params]
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
throw new Error("Unknown AST node type");
|
|
44
|
-
}
|
|
45
|
-
var createColumnProxy = () => new Proxy({}, {
|
|
46
|
-
get: (_, prop) => ({ type: "column", name: prop })
|
|
47
|
-
});
|
|
48
|
-
var createFunctionProxy = () => new Proxy({}, {
|
|
49
|
-
get: (_, funcName) => (...args) => ({
|
|
50
|
-
type: "function",
|
|
51
|
-
name: funcName.toUpperCase(),
|
|
52
|
-
args: args.map(wrapNode)
|
|
53
|
-
})
|
|
54
|
-
});
|
|
55
|
-
var op = {
|
|
56
|
-
eq: (left, right) => ({ type: "operator", op: "=", left: wrapNode(left), right: wrapNode(right) }),
|
|
57
|
-
ne: (left, right) => ({ type: "operator", op: "!=", left: wrapNode(left), right: wrapNode(right) }),
|
|
58
|
-
gt: (left, right) => ({ type: "operator", op: ">", left: wrapNode(left), right: wrapNode(right) }),
|
|
59
|
-
gte: (left, right) => ({ type: "operator", op: ">=", left: wrapNode(left), right: wrapNode(right) }),
|
|
60
|
-
lt: (left, right) => ({ type: "operator", op: "<", left: wrapNode(left), right: wrapNode(right) }),
|
|
61
|
-
lte: (left, right) => ({ type: "operator", op: "<=", left: wrapNode(left), right: wrapNode(right) }),
|
|
62
|
-
and: (left, right) => ({ type: "operator", op: "AND", left: wrapNode(left), right: wrapNode(right) }),
|
|
63
|
-
or: (left, right) => ({ type: "operator", op: "OR", left: wrapNode(left), right: wrapNode(right) }),
|
|
64
|
-
like: (left, right) => ({ type: "operator", op: "LIKE", left: wrapNode(left), right: wrapNode(right) }),
|
|
65
|
-
isNull: (node) => ({ type: "operator", op: "IS", left: wrapNode(node), right: { type: "literal", value: null } }),
|
|
66
|
-
isNotNull: (node) => ({ type: "operator", op: "IS NOT", left: wrapNode(node), right: { type: "literal", value: null } }),
|
|
67
|
-
in: (left, values) => ({
|
|
68
|
-
type: "function",
|
|
69
|
-
name: `${compileAST(wrapNode(left)).sql} IN`,
|
|
70
|
-
args: values.map((v) => wrapNode(v))
|
|
71
|
-
}),
|
|
72
|
-
not: (node) => ({ type: "operator", op: "NOT", left: { type: "literal", value: "" }, right: wrapNode(node) })
|
|
73
|
-
};
|
|
74
|
-
|
|
75
16
|
// node_modules/zod/v3/external.js
|
|
76
17
|
var exports_external = {};
|
|
77
18
|
__export(exports_external, {
|
|
@@ -4139,7 +4080,66 @@ function transformFromStorage(row, schema) {
|
|
|
4139
4080
|
return transformed;
|
|
4140
4081
|
}
|
|
4141
4082
|
|
|
4142
|
-
// src/
|
|
4083
|
+
// src/ast.ts
|
|
4084
|
+
var wrapNode = (val) => val !== null && typeof val === "object" && ("type" in val) ? val : { type: "literal", value: val };
|
|
4085
|
+
function compileAST(node) {
|
|
4086
|
+
if (node.type === "column")
|
|
4087
|
+
return { sql: `"${node.name}"`, params: [] };
|
|
4088
|
+
if (node.type === "literal") {
|
|
4089
|
+
if (node.value instanceof Date)
|
|
4090
|
+
return { sql: "?", params: [node.value.toISOString()] };
|
|
4091
|
+
if (typeof node.value === "boolean")
|
|
4092
|
+
return { sql: "?", params: [node.value ? 1 : 0] };
|
|
4093
|
+
return { sql: "?", params: [node.value] };
|
|
4094
|
+
}
|
|
4095
|
+
if (node.type === "function") {
|
|
4096
|
+
const compiledArgs = node.args.map(compileAST);
|
|
4097
|
+
return {
|
|
4098
|
+
sql: `${node.name}(${compiledArgs.map((c) => c.sql).join(", ")})`,
|
|
4099
|
+
params: compiledArgs.flatMap((c) => c.params)
|
|
4100
|
+
};
|
|
4101
|
+
}
|
|
4102
|
+
if (node.type === "operator") {
|
|
4103
|
+
const left = compileAST(node.left);
|
|
4104
|
+
const right = compileAST(node.right);
|
|
4105
|
+
return {
|
|
4106
|
+
sql: `(${left.sql} ${node.op} ${right.sql})`,
|
|
4107
|
+
params: [...left.params, ...right.params]
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
throw new Error("Unknown AST node type");
|
|
4111
|
+
}
|
|
4112
|
+
var createColumnProxy = () => new Proxy({}, {
|
|
4113
|
+
get: (_, prop) => ({ type: "column", name: prop })
|
|
4114
|
+
});
|
|
4115
|
+
var createFunctionProxy = () => new Proxy({}, {
|
|
4116
|
+
get: (_, funcName) => (...args) => ({
|
|
4117
|
+
type: "function",
|
|
4118
|
+
name: funcName.toUpperCase(),
|
|
4119
|
+
args: args.map(wrapNode)
|
|
4120
|
+
})
|
|
4121
|
+
});
|
|
4122
|
+
var op = {
|
|
4123
|
+
eq: (left, right) => ({ type: "operator", op: "=", left: wrapNode(left), right: wrapNode(right) }),
|
|
4124
|
+
ne: (left, right) => ({ type: "operator", op: "!=", left: wrapNode(left), right: wrapNode(right) }),
|
|
4125
|
+
gt: (left, right) => ({ type: "operator", op: ">", left: wrapNode(left), right: wrapNode(right) }),
|
|
4126
|
+
gte: (left, right) => ({ type: "operator", op: ">=", left: wrapNode(left), right: wrapNode(right) }),
|
|
4127
|
+
lt: (left, right) => ({ type: "operator", op: "<", left: wrapNode(left), right: wrapNode(right) }),
|
|
4128
|
+
lte: (left, right) => ({ type: "operator", op: "<=", left: wrapNode(left), right: wrapNode(right) }),
|
|
4129
|
+
and: (left, right) => ({ type: "operator", op: "AND", left: wrapNode(left), right: wrapNode(right) }),
|
|
4130
|
+
or: (left, right) => ({ type: "operator", op: "OR", left: wrapNode(left), right: wrapNode(right) }),
|
|
4131
|
+
like: (left, right) => ({ type: "operator", op: "LIKE", left: wrapNode(left), right: wrapNode(right) }),
|
|
4132
|
+
isNull: (node) => ({ type: "operator", op: "IS", left: wrapNode(node), right: { type: "literal", value: null } }),
|
|
4133
|
+
isNotNull: (node) => ({ type: "operator", op: "IS NOT", left: wrapNode(node), right: { type: "literal", value: null } }),
|
|
4134
|
+
in: (left, values) => ({
|
|
4135
|
+
type: "function",
|
|
4136
|
+
name: `${compileAST(wrapNode(left)).sql} IN`,
|
|
4137
|
+
args: values.map((v) => wrapNode(v))
|
|
4138
|
+
}),
|
|
4139
|
+
not: (node) => ({ type: "operator", op: "NOT", left: { type: "literal", value: "" }, right: wrapNode(node) })
|
|
4140
|
+
};
|
|
4141
|
+
|
|
4142
|
+
// src/iqo.ts
|
|
4143
4143
|
var OPERATOR_MAP = {
|
|
4144
4144
|
$gt: ">",
|
|
4145
4145
|
$gte: ">=",
|
|
@@ -4251,7 +4251,7 @@ function compileIQO(tableName, iqo) {
|
|
|
4251
4251
|
sql += ` OFFSET ${iqo.offset}`;
|
|
4252
4252
|
return { sql, params };
|
|
4253
4253
|
}
|
|
4254
|
-
|
|
4254
|
+
// src/builder.ts
|
|
4255
4255
|
class QueryBuilder {
|
|
4256
4256
|
iqo;
|
|
4257
4257
|
tableName;
|
|
@@ -4442,7 +4442,7 @@ class QueryBuilder {
|
|
|
4442
4442
|
}
|
|
4443
4443
|
}
|
|
4444
4444
|
}
|
|
4445
|
-
|
|
4445
|
+
// src/proxy.ts
|
|
4446
4446
|
class ColumnNode {
|
|
4447
4447
|
table;
|
|
4448
4448
|
column;
|
|
@@ -4550,7 +4550,7 @@ function compileProxyQuery(queryResult, aliasMap) {
|
|
|
4550
4550
|
const whereParts = [];
|
|
4551
4551
|
for (const [key, value] of Object.entries(queryResult.where)) {
|
|
4552
4552
|
let fieldRef;
|
|
4553
|
-
const quotedMatch = key.match(/^"([^"]+)"
|
|
4553
|
+
const quotedMatch = key.match(/^"([^"]+)"\."([^"]+)"$/);
|
|
4554
4554
|
if (quotedMatch && tablesUsed.has(quotedMatch[1])) {
|
|
4555
4555
|
fieldRef = key;
|
|
4556
4556
|
} else {
|
|
@@ -4605,7 +4605,7 @@ function compileProxyQuery(queryResult, aliasMap) {
|
|
|
4605
4605
|
const parts = [];
|
|
4606
4606
|
for (const [key, dir] of Object.entries(queryResult.orderBy)) {
|
|
4607
4607
|
let fieldRef;
|
|
4608
|
-
const quotedMatch = key.match(/^"([^"]+)"
|
|
4608
|
+
const quotedMatch = key.match(/^"([^"]+)"\."([^"]+)"$/);
|
|
4609
4609
|
if (quotedMatch && tablesUsed.has(quotedMatch[1])) {
|
|
4610
4610
|
fieldRef = key;
|
|
4611
4611
|
} else {
|
|
@@ -4633,6 +4633,8 @@ function executeProxyQuery(schemas, callback, executor) {
|
|
|
4633
4633
|
const { sql, params } = compileProxyQuery(queryResult, aliasMap);
|
|
4634
4634
|
return executor(sql, params);
|
|
4635
4635
|
}
|
|
4636
|
+
|
|
4637
|
+
// src/query.ts
|
|
4636
4638
|
function createQueryBuilder(ctx, entityName, initialCols) {
|
|
4637
4639
|
const schema = ctx.schemas[entityName];
|
|
4638
4640
|
const executor = (sql, params, raw) => {
|
|
@@ -4864,6 +4866,24 @@ function upsert(ctx, entityName, data, conditions = {}) {
|
|
|
4864
4866
|
function deleteEntity(ctx, entityName, id) {
|
|
4865
4867
|
ctx.db.query(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
|
|
4866
4868
|
}
|
|
4869
|
+
function deleteWhere(ctx, entityName, conditions) {
|
|
4870
|
+
const { clause, values } = ctx.buildWhereClause(conditions);
|
|
4871
|
+
if (!clause)
|
|
4872
|
+
throw new Error("delete().where() requires at least one condition");
|
|
4873
|
+
const result = ctx.db.query(`DELETE FROM "${entityName}" ${clause}`).run(...values);
|
|
4874
|
+
return result.changes ?? 0;
|
|
4875
|
+
}
|
|
4876
|
+
function createDeleteBuilder(ctx, entityName) {
|
|
4877
|
+
let _conditions = {};
|
|
4878
|
+
const builder = {
|
|
4879
|
+
where: (conditions) => {
|
|
4880
|
+
_conditions = { ..._conditions, ...conditions };
|
|
4881
|
+
return builder;
|
|
4882
|
+
},
|
|
4883
|
+
exec: () => deleteWhere(ctx, entityName, _conditions)
|
|
4884
|
+
};
|
|
4885
|
+
return builder;
|
|
4886
|
+
}
|
|
4867
4887
|
function insertMany(ctx, entityName, rows) {
|
|
4868
4888
|
if (rows.length === 0)
|
|
4869
4889
|
return [];
|
|
@@ -4965,7 +4985,11 @@ class _Database {
|
|
|
4965
4985
|
return createUpdateBuilder(this._ctx, entityName, idOrData);
|
|
4966
4986
|
},
|
|
4967
4987
|
upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
|
|
4968
|
-
delete: (id) =>
|
|
4988
|
+
delete: (id) => {
|
|
4989
|
+
if (typeof id === "number")
|
|
4990
|
+
return deleteEntity(this._ctx, entityName, id);
|
|
4991
|
+
return createDeleteBuilder(this._ctx, entityName);
|
|
4992
|
+
},
|
|
4969
4993
|
select: (...cols) => createQueryBuilder(this._ctx, entityName, cols),
|
|
4970
4994
|
on: (event, callback) => {
|
|
4971
4995
|
return this._registerListener(entityName, event, callback);
|
package/package.json
CHANGED
package/src/builder.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builder.ts — Fluent QueryBuilder class
|
|
3
|
+
*
|
|
4
|
+
* Accumulates query state via chaining and executes when a
|
|
5
|
+
* terminal method (.all(), .get(), .count()) is called.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ASTNode, type WhereCallback, type TypedColumnProxy, type FunctionProxy, type Operators,
|
|
10
|
+
createColumnProxy, createFunctionProxy, op,
|
|
11
|
+
} from './ast';
|
|
12
|
+
import {
|
|
13
|
+
type IQO, type WhereCondition, type WhereOperator, type OrderDirection,
|
|
14
|
+
OPERATOR_MAP, compileIQO,
|
|
15
|
+
} from './iqo';
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// QueryBuilder Class
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A Fluent Query Builder that accumulates query state via chaining
|
|
23
|
+
* and only executes when a terminal method is called (.all(), .get())
|
|
24
|
+
* or when it is `await`-ed (thenable).
|
|
25
|
+
*
|
|
26
|
+
* Supports two WHERE styles:
|
|
27
|
+
* - Object-style: `.where({ name: 'Alice', age: { $gt: 18 } })`
|
|
28
|
+
* - Callback-style (AST): `.where((c, f, op) => op.and(op.eq(c.name, 'Alice'), op.gt(c.age, 18)))`
|
|
29
|
+
*/
|
|
30
|
+
export class QueryBuilder<T extends Record<string, any>> {
|
|
31
|
+
private iqo: IQO;
|
|
32
|
+
private tableName: string;
|
|
33
|
+
private executor: (sql: string, params: any[], raw: boolean) => any[];
|
|
34
|
+
private singleExecutor: (sql: string, params: any[], raw: boolean) => any | null;
|
|
35
|
+
private joinResolver: ((fromTable: string, toTable: string) => { fk: string; pk: string } | null) | null;
|
|
36
|
+
private conditionResolver: ((conditions: Record<string, any>) => Record<string, any>) | null;
|
|
37
|
+
private eagerLoader: ((parentTable: string, relation: string, parentIds: number[]) => { key: string; groups: Map<number, any[]> } | null) | null;
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
tableName: string,
|
|
41
|
+
executor: (sql: string, params: any[], raw: boolean) => any[],
|
|
42
|
+
singleExecutor: (sql: string, params: any[], raw: boolean) => any | null,
|
|
43
|
+
joinResolver?: ((fromTable: string, toTable: string) => { fk: string; pk: string } | null) | null,
|
|
44
|
+
conditionResolver?: ((conditions: Record<string, any>) => Record<string, any>) | null,
|
|
45
|
+
eagerLoader?: ((parentTable: string, relation: string, parentIds: number[]) => { key: string; groups: Map<number, any[]> } | null) | null,
|
|
46
|
+
) {
|
|
47
|
+
this.tableName = tableName;
|
|
48
|
+
this.executor = executor;
|
|
49
|
+
this.singleExecutor = singleExecutor;
|
|
50
|
+
this.joinResolver = joinResolver ?? null;
|
|
51
|
+
this.conditionResolver = conditionResolver ?? null;
|
|
52
|
+
this.eagerLoader = eagerLoader ?? null;
|
|
53
|
+
this.iqo = {
|
|
54
|
+
selects: [],
|
|
55
|
+
wheres: [],
|
|
56
|
+
whereOrs: [],
|
|
57
|
+
whereAST: null,
|
|
58
|
+
joins: [],
|
|
59
|
+
groupBy: [],
|
|
60
|
+
limit: null,
|
|
61
|
+
offset: null,
|
|
62
|
+
orderBy: [],
|
|
63
|
+
includes: [],
|
|
64
|
+
raw: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Specify which columns to select. If called with no arguments, defaults to `*`. */
|
|
69
|
+
select(...cols: (keyof T & string)[]): this {
|
|
70
|
+
this.iqo.selects.push(...cols);
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Add WHERE conditions. Two calling styles:
|
|
76
|
+
*
|
|
77
|
+
* **Object-style** (simple equality and operators):
|
|
78
|
+
* ```ts
|
|
79
|
+
* .where({ name: 'Alice' })
|
|
80
|
+
* .where({ age: { $gt: 18 } })
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* **Callback-style** (AST-based, full SQL expression power):
|
|
84
|
+
* ```ts
|
|
85
|
+
* .where((c, f, op) => op.and(
|
|
86
|
+
* op.eq(f.lower(c.name), 'alice'),
|
|
87
|
+
* op.gt(c.age, 18)
|
|
88
|
+
* ))
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
where(criteriaOrCallback: (Partial<Record<keyof T & string, any>> & { $or?: Partial<Record<keyof T & string, any>>[] }) | WhereCallback<T>): this {
|
|
92
|
+
if (typeof criteriaOrCallback === 'function') {
|
|
93
|
+
const ast = (criteriaOrCallback as WhereCallback<T>)(
|
|
94
|
+
createColumnProxy<T>(),
|
|
95
|
+
createFunctionProxy(),
|
|
96
|
+
op,
|
|
97
|
+
);
|
|
98
|
+
if (this.iqo.whereAST) {
|
|
99
|
+
this.iqo.whereAST = { type: 'operator', op: 'AND', left: this.iqo.whereAST, right: ast };
|
|
100
|
+
} else {
|
|
101
|
+
this.iqo.whereAST = ast;
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
const resolved = this.conditionResolver
|
|
105
|
+
? this.conditionResolver(criteriaOrCallback as Record<string, any>)
|
|
106
|
+
: criteriaOrCallback;
|
|
107
|
+
|
|
108
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
109
|
+
if (key === '$or' && Array.isArray(value)) {
|
|
110
|
+
const orConditions: WhereCondition[] = [];
|
|
111
|
+
for (const branch of value as Record<string, any>[]) {
|
|
112
|
+
const resolvedBranch = this.conditionResolver
|
|
113
|
+
? this.conditionResolver(branch)
|
|
114
|
+
: branch;
|
|
115
|
+
for (const [bKey, bValue] of Object.entries(resolvedBranch)) {
|
|
116
|
+
if (typeof bValue === 'object' && bValue !== null && !Array.isArray(bValue) && !(bValue instanceof Date)) {
|
|
117
|
+
for (const [opKey, operand] of Object.entries(bValue)) {
|
|
118
|
+
const sqlOp = OPERATOR_MAP[opKey as WhereOperator];
|
|
119
|
+
if (sqlOp) orConditions.push({ field: bKey, operator: sqlOp as WhereCondition['operator'], value: operand });
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
orConditions.push({ field: bKey, operator: '=', value: bValue });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (orConditions.length > 0) this.iqo.whereOrs.push(orConditions);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
|
|
131
|
+
for (const [opKey, operand] of Object.entries(value)) {
|
|
132
|
+
const sqlOp = OPERATOR_MAP[opKey as WhereOperator];
|
|
133
|
+
if (!sqlOp) throw new Error(`Unsupported query operator: '${opKey}' on field '${key}'.`);
|
|
134
|
+
if (opKey === '$between') {
|
|
135
|
+
if (!Array.isArray(operand) || operand.length !== 2) throw new Error(`$between for '${key}' requires [min, max]`);
|
|
136
|
+
}
|
|
137
|
+
this.iqo.wheres.push({
|
|
138
|
+
field: key,
|
|
139
|
+
operator: sqlOp as WhereCondition['operator'],
|
|
140
|
+
value: operand,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
this.iqo.wheres.push({ field: key, operator: '=', value });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Set the maximum number of rows to return. */
|
|
152
|
+
limit(n: number): this {
|
|
153
|
+
this.iqo.limit = n;
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Set the offset for pagination. */
|
|
158
|
+
offset(n: number): this {
|
|
159
|
+
this.iqo.offset = n;
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Add ORDER BY clauses. */
|
|
164
|
+
orderBy(field: keyof T & string, direction: OrderDirection = 'asc'): this {
|
|
165
|
+
this.iqo.orderBy.push({ field, direction });
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Join another table. Two calling styles:
|
|
171
|
+
*
|
|
172
|
+
* **Accessor-based** (auto-infers FK from relationships):
|
|
173
|
+
* ```ts
|
|
174
|
+
* db.trees.select('name').join(db.forests, ['name']).all()
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* **String-based** (manual FK):
|
|
178
|
+
* ```ts
|
|
179
|
+
* db.trees.select('name').join('forests', 'forestId', ['name']).all()
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
join(accessor: { _tableName: string }, columns?: string[]): this;
|
|
183
|
+
join(table: string, fk: string, columns?: string[], pk?: string): this;
|
|
184
|
+
join(tableOrAccessor: string | { _tableName: string }, fkOrCols?: string | string[], colsOrPk?: string[] | string, pk?: string): this {
|
|
185
|
+
let table: string;
|
|
186
|
+
let fromCol: string;
|
|
187
|
+
let toCol: string;
|
|
188
|
+
let columns: string[];
|
|
189
|
+
|
|
190
|
+
if (typeof tableOrAccessor === 'object' && '_tableName' in tableOrAccessor) {
|
|
191
|
+
table = tableOrAccessor._tableName;
|
|
192
|
+
columns = Array.isArray(fkOrCols) ? fkOrCols : [];
|
|
193
|
+
if (!this.joinResolver) throw new Error(`Cannot auto-resolve join: no relationship data available`);
|
|
194
|
+
const resolved = this.joinResolver(this.tableName, table);
|
|
195
|
+
if (!resolved) throw new Error(`No relationship found between '${this.tableName}' and '${table}'`);
|
|
196
|
+
fromCol = resolved.fk;
|
|
197
|
+
toCol = resolved.pk;
|
|
198
|
+
} else {
|
|
199
|
+
table = tableOrAccessor;
|
|
200
|
+
fromCol = fkOrCols as string;
|
|
201
|
+
columns = Array.isArray(colsOrPk) ? colsOrPk : [];
|
|
202
|
+
toCol = (typeof colsOrPk === 'string' ? colsOrPk : pk) ?? 'id';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.iqo.joins.push({ table, fromCol, toCol, columns });
|
|
206
|
+
this.iqo.raw = true;
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Skip Zod parsing and return raw SQLite row objects. */
|
|
211
|
+
raw(): this {
|
|
212
|
+
this.iqo.raw = true;
|
|
213
|
+
return this;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Eagerly load a related entity and attach as an array property.
|
|
218
|
+
*
|
|
219
|
+
* Runs a single batched query (WHERE fk IN (...)) per relation,
|
|
220
|
+
* avoiding the N+1 problem of lazy navigation.
|
|
221
|
+
*/
|
|
222
|
+
with(...relations: string[]): this {
|
|
223
|
+
this.iqo.includes.push(...relations);
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Internal: apply eager loads to a set of results */
|
|
228
|
+
private _applyEagerLoads(results: T[]): T[] {
|
|
229
|
+
if (this.iqo.includes.length === 0 || !this.eagerLoader || results.length === 0) {
|
|
230
|
+
return results;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const parentIds = results.map((r: any) => r.id).filter((id: any) => typeof id === 'number');
|
|
234
|
+
if (parentIds.length === 0) return results;
|
|
235
|
+
|
|
236
|
+
for (const relation of this.iqo.includes) {
|
|
237
|
+
const loaded = this.eagerLoader(this.tableName, relation, parentIds);
|
|
238
|
+
if (!loaded) continue;
|
|
239
|
+
|
|
240
|
+
for (const row of results as any[]) {
|
|
241
|
+
row[loaded.key] = loaded.groups.get(row.id) ?? [];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------- Terminal / Execution Methods ----------
|
|
249
|
+
|
|
250
|
+
/** Execute the query and return all matching rows. */
|
|
251
|
+
all(): T[] {
|
|
252
|
+
const { sql, params } = compileIQO(this.tableName, this.iqo);
|
|
253
|
+
const results = this.executor(sql, params, this.iqo.raw);
|
|
254
|
+
return this._applyEagerLoads(results);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Execute the query and return the first matching row, or null. */
|
|
258
|
+
get(): T | null {
|
|
259
|
+
this.iqo.limit = 1;
|
|
260
|
+
const { sql, params } = compileIQO(this.tableName, this.iqo);
|
|
261
|
+
const result = this.singleExecutor(sql, params, this.iqo.raw);
|
|
262
|
+
if (!result) return null;
|
|
263
|
+
const [loaded] = this._applyEagerLoads([result]);
|
|
264
|
+
return loaded ?? null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Execute the query and return the count of matching rows. */
|
|
268
|
+
count(): number {
|
|
269
|
+
// Reuse compileIQO to avoid duplicating WHERE logic
|
|
270
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
271
|
+
// Replace "SELECT ... FROM" with "SELECT COUNT(*) as count FROM"
|
|
272
|
+
const countSql = selectSql.replace(/^SELECT .+? FROM/, 'SELECT COUNT(*) as count FROM');
|
|
273
|
+
const results = this.executor(countSql, params, true);
|
|
274
|
+
return (results[0] as any)?.count ?? 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Alias for get() — returns the first matching row or null. */
|
|
278
|
+
first(): T | null {
|
|
279
|
+
return this.get();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Returns true if at least one row matches the query. */
|
|
283
|
+
exists(): boolean {
|
|
284
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
285
|
+
const existsSql = selectSql.replace(/^SELECT .+? FROM/, 'SELECT 1 FROM').replace(/ LIMIT \d+/, '') + ' LIMIT 1';
|
|
286
|
+
const results = this.executor(existsSql, params, true);
|
|
287
|
+
return results.length > 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Group results by one or more columns. */
|
|
291
|
+
groupBy(...fields: string[]): this {
|
|
292
|
+
this.iqo.groupBy.push(...fields);
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
// ---------- Thenable (async/await support) ----------
|
|
299
|
+
|
|
300
|
+
then<TResult1 = T[], TResult2 = never>(
|
|
301
|
+
onfulfilled?: ((value: T[]) => TResult1 | PromiseLike<TResult1>) | null,
|
|
302
|
+
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
|
|
303
|
+
): Promise<TResult1 | TResult2> {
|
|
304
|
+
try {
|
|
305
|
+
const result = this.all();
|
|
306
|
+
return Promise.resolve(result).then(onfulfilled, onrejected);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
return Promise.reject(err).then(onfulfilled, onrejected);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
package/src/crud.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Each function accepts a `DatabaseContext` so it can access
|
|
5
5
|
* the db handle, schemas, and entity methods without tight coupling.
|
|
6
6
|
*/
|
|
7
|
-
import type { AugmentedEntity, UpdateBuilder } from './types';
|
|
7
|
+
import type { AugmentedEntity, UpdateBuilder, DeleteBuilder } from './types';
|
|
8
8
|
import { asZodObject } from './types';
|
|
9
9
|
import { transformForStorage, transformFromStorage } from './schema';
|
|
10
10
|
import type { DatabaseContext } from './context';
|
|
@@ -118,6 +118,24 @@ export function deleteEntity(ctx: DatabaseContext, entityName: string, id: numbe
|
|
|
118
118
|
ctx.db.query(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/** Delete all rows matching the given conditions. Returns the number of rows deleted. */
|
|
122
|
+
export function deleteWhere(ctx: DatabaseContext, entityName: string, conditions: Record<string, any>): number {
|
|
123
|
+
const { clause, values } = ctx.buildWhereClause(conditions);
|
|
124
|
+
if (!clause) throw new Error('delete().where() requires at least one condition');
|
|
125
|
+
const result = ctx.db.query(`DELETE FROM "${entityName}" ${clause}`).run(...values);
|
|
126
|
+
return (result as any).changes ?? 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Create a fluent delete builder: db.table.delete().where({...}).exec() */
|
|
130
|
+
export function createDeleteBuilder(ctx: DatabaseContext, entityName: string): DeleteBuilder<any> {
|
|
131
|
+
let _conditions: Record<string, any> = {};
|
|
132
|
+
const builder: DeleteBuilder<any> = {
|
|
133
|
+
where: (conditions) => { _conditions = { ..._conditions, ...conditions }; return builder; },
|
|
134
|
+
exec: () => deleteWhere(ctx, entityName, _conditions),
|
|
135
|
+
};
|
|
136
|
+
return builder;
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
/** Insert multiple rows in a single transaction for better performance. */
|
|
122
140
|
export function insertMany<T extends Record<string, any>>(ctx: DatabaseContext, entityName: string, rows: Omit<T, 'id'>[]): AugmentedEntity<any>[] {
|
|
123
141
|
if (rows.length === 0) return [];
|
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, deleteEntity,
|
|
27
|
+
insert, insertMany, update, upsert, deleteEntity, createDeleteBuilder,
|
|
28
28
|
getById, getOne, findMany, updateWhere, createUpdateBuilder,
|
|
29
29
|
} from './crud';
|
|
30
30
|
|
|
@@ -95,7 +95,10 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
95
95
|
return createUpdateBuilder(this._ctx, entityName, idOrData);
|
|
96
96
|
},
|
|
97
97
|
upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
|
|
98
|
-
delete: (id) =>
|
|
98
|
+
delete: ((id?: any) => {
|
|
99
|
+
if (typeof id === 'number') return deleteEntity(this._ctx, entityName, id);
|
|
100
|
+
return createDeleteBuilder(this._ctx, entityName);
|
|
101
|
+
}) as any,
|
|
99
102
|
select: (...cols: string[]) => createQueryBuilder(this._ctx, entityName, cols),
|
|
100
103
|
on: (event: ChangeEvent, callback: (row: any) => void | Promise<void>) => {
|
|
101
104
|
return this._registerListener(entityName, event, callback);
|