@dockstat/sqlite-wrapper 1.2.7 → 1.3.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/README.md +493 -39
- package/index.ts +436 -156
- package/package.json +11 -5
- package/query-builder/base.ts +103 -141
- package/query-builder/delete.ts +276 -187
- package/query-builder/index.ts +95 -117
- package/query-builder/insert.ts +184 -153
- package/query-builder/select.ts +155 -180
- package/query-builder/update.ts +195 -165
- package/query-builder/where.ts +165 -200
- package/types.ts +134 -149
- package/utils/index.ts +44 -0
- package/utils/logger.ts +184 -0
- package/utils/sql.ts +241 -0
- package/utils/transformer.ts +256 -0
package/query-builder/where.ts
CHANGED
|
@@ -1,307 +1,272 @@
|
|
|
1
|
-
import type { SQLQueryBindings } from "bun:sqlite"
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
1
|
+
import type { SQLQueryBindings } from "bun:sqlite"
|
|
2
|
+
import type { RegexCondition, WhereCondition } from "../types"
|
|
3
|
+
import { buildBetweenClause, buildInClause, normalizeOperator, quoteIdentifier } from "../utils"
|
|
4
|
+
import { BaseQueryBuilder } from "./base"
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* WhereQueryBuilder - Adds WHERE clause functionality to the QueryBuilder
|
|
8
|
+
*
|
|
9
|
+
* Provides methods for building SQL WHERE conditions:
|
|
10
|
+
* - Simple equality conditions
|
|
11
|
+
* - Comparison operators
|
|
12
|
+
* - IN/NOT IN clauses
|
|
13
|
+
* - BETWEEN clauses
|
|
14
|
+
* - NULL checks
|
|
15
|
+
* - Raw SQL expressions
|
|
16
|
+
* - Regex conditions (client-side filtering)
|
|
8
17
|
*/
|
|
9
|
-
export class WhereQueryBuilder<
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
export class WhereQueryBuilder<T extends Record<string, unknown>> extends BaseQueryBuilder<T> {
|
|
19
|
+
// ===== Private Helpers =====
|
|
20
|
+
|
|
12
21
|
/**
|
|
13
|
-
* Remove existing condition for a column
|
|
14
|
-
* @param column - Column name to check
|
|
15
|
-
* @param operation - Optional operation type (e.g., '=', 'IN', 'BETWEEN')
|
|
22
|
+
* Remove an existing condition for a column to prevent duplicates
|
|
16
23
|
*/
|
|
17
24
|
private removeExistingCondition(column: string, operation?: string): void {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
condition.startsWith(`${String(column)} ${operation}`)
|
|
24
|
-
);
|
|
25
|
-
} else {
|
|
26
|
-
// Look for any condition on this column
|
|
27
|
-
existingIndex = this.state.whereConditions.findIndex(condition =>
|
|
28
|
-
condition.startsWith(`${String(column)} `)
|
|
29
|
-
);
|
|
30
|
-
}
|
|
25
|
+
const columnPattern = operation ? `${column} ${operation}` : `${column} `
|
|
26
|
+
|
|
27
|
+
const existingIndex = this.state.whereConditions.findIndex(
|
|
28
|
+
(condition) => condition.startsWith(columnPattern) || condition.startsWith(`"${column}"`)
|
|
29
|
+
)
|
|
31
30
|
|
|
32
31
|
if (existingIndex !== -1) {
|
|
33
|
-
this.state.whereConditions.splice(existingIndex, 1)
|
|
34
|
-
//
|
|
32
|
+
this.state.whereConditions.splice(existingIndex, 1)
|
|
33
|
+
// Remove corresponding params if they exist
|
|
35
34
|
if (existingIndex < this.state.whereParams.length) {
|
|
36
|
-
this.state.whereParams.splice(existingIndex, 1)
|
|
35
|
+
this.state.whereParams.splice(existingIndex, 1)
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
// Also remove any regex conditions for this column
|
|
41
40
|
this.state.regexConditions = this.state.regexConditions.filter(
|
|
42
|
-
cond => String(cond.column) !== column
|
|
43
|
-
)
|
|
41
|
+
(cond) => String(cond.column) !== column
|
|
42
|
+
)
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
/**
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
* Convert a JavaScript value to SQLite-compatible value
|
|
47
|
+
*/
|
|
48
|
+
private toSqliteValue(value: unknown): SQLQueryBindings {
|
|
49
|
+
if (typeof value === "boolean") {
|
|
50
|
+
return value ? 1 : 0
|
|
51
|
+
}
|
|
52
|
+
return value as SQLQueryBindings
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ===== Public WHERE Methods =====
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add simple equality conditions to the WHERE clause
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* .where({ name: "Alice", active: true })
|
|
62
|
+
* // WHERE "name" = ? AND "active" = ?
|
|
50
63
|
*
|
|
51
|
-
* @
|
|
52
|
-
*
|
|
64
|
+
* @example
|
|
65
|
+
* .where({ deleted_at: null })
|
|
66
|
+
* // WHERE "deleted_at" IS NULL
|
|
53
67
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// Convert JavaScript boolean to SQLite integer (0/1)
|
|
65
|
-
let sqliteValue: SQLQueryBindings = value;
|
|
66
|
-
if (typeof value === 'boolean') {
|
|
67
|
-
sqliteValue = value ? 1 : 0;
|
|
68
|
-
this.getLogger().debug(`Converting boolean value ${value} to ${sqliteValue} for column ${column}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
this.state.whereParams.push(sqliteValue);
|
|
72
|
-
}
|
|
68
|
+
where(conditions: WhereCondition<T>): this {
|
|
69
|
+
for (const [column, value] of Object.entries(conditions)) {
|
|
70
|
+
this.removeExistingCondition(column)
|
|
71
|
+
|
|
72
|
+
if (value === null || value === undefined) {
|
|
73
|
+
this.state.whereConditions.push(`${quoteIdentifier(column)} IS NULL`)
|
|
74
|
+
} else {
|
|
75
|
+
this.state.whereConditions.push(`${quoteIdentifier(column)} = ?`)
|
|
76
|
+
this.state.whereParams.push(this.toSqliteValue(value))
|
|
73
77
|
}
|
|
74
|
-
return this;
|
|
75
78
|
}
|
|
79
|
+
return this
|
|
80
|
+
}
|
|
76
81
|
|
|
77
82
|
/**
|
|
78
|
-
* Add regex conditions
|
|
79
|
-
* after SQL execution due to Bun's SQLite limitations.
|
|
80
|
-
* Prevents duplicate regex conditions for the same column.
|
|
83
|
+
* Add regex conditions (applied client-side after SQL execution)
|
|
81
84
|
*
|
|
82
|
-
* @
|
|
83
|
-
*
|
|
85
|
+
* @example
|
|
86
|
+
* .whereRgx({ email: /@gmail\.com$/ })
|
|
84
87
|
*/
|
|
85
88
|
whereRgx(conditions: RegexCondition<T>): this {
|
|
86
89
|
for (const [column, value] of Object.entries(conditions)) {
|
|
87
|
-
|
|
88
|
-
this.removeExistingCondition(column);
|
|
90
|
+
this.removeExistingCondition(column)
|
|
89
91
|
|
|
90
92
|
if (value instanceof RegExp) {
|
|
91
93
|
this.state.regexConditions.push({
|
|
92
94
|
column: column as keyof T,
|
|
93
95
|
regex: value,
|
|
94
|
-
})
|
|
96
|
+
})
|
|
95
97
|
} else if (typeof value === "string") {
|
|
96
98
|
this.state.regexConditions.push({
|
|
97
99
|
column: column as keyof T,
|
|
98
100
|
regex: new RegExp(value),
|
|
99
|
-
})
|
|
101
|
+
})
|
|
100
102
|
} else if (value !== null && value !== undefined) {
|
|
101
|
-
|
|
102
|
-
this.state.
|
|
103
|
+
// Fall back to equality check for non-regex values
|
|
104
|
+
this.state.whereConditions.push(`${quoteIdentifier(column)} = ?`)
|
|
105
|
+
this.state.whereParams.push(value as SQLQueryBindings)
|
|
103
106
|
}
|
|
104
107
|
}
|
|
105
|
-
return this
|
|
108
|
+
return this
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
/**
|
|
109
|
-
* Add a raw SQL WHERE
|
|
110
|
-
* Note: Raw expressions bypass duplicate checking as they may be complex conditions.
|
|
112
|
+
* Add a raw SQL WHERE expression with parameter binding
|
|
111
113
|
*
|
|
112
|
-
* @
|
|
113
|
-
*
|
|
114
|
-
*
|
|
114
|
+
* @example
|
|
115
|
+
* .whereExpr("LENGTH(name) > ?", [5])
|
|
116
|
+
* .whereExpr("created_at > datetime('now', '-1 day')")
|
|
115
117
|
*/
|
|
116
118
|
whereExpr(expr: string, params: SQLQueryBindings[] = []): this {
|
|
117
119
|
if (!expr || typeof expr !== "string") {
|
|
118
|
-
throw new Error("whereExpr:
|
|
120
|
+
throw new Error("whereExpr: expression must be a non-empty string")
|
|
119
121
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
|
|
123
|
+
// Wrap in parentheses to preserve grouping
|
|
124
|
+
this.state.whereConditions.push(`(${expr})`)
|
|
125
|
+
|
|
126
|
+
if (params.length > 0) {
|
|
127
|
+
this.state.whereParams.push(...params)
|
|
124
128
|
}
|
|
125
|
-
|
|
129
|
+
|
|
130
|
+
return this
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
/**
|
|
129
|
-
* Alias for whereExpr
|
|
134
|
+
* Alias for whereExpr
|
|
130
135
|
*/
|
|
131
136
|
whereRaw(expr: string, params: SQLQueryBindings[] = []): this {
|
|
132
|
-
return this.whereExpr(expr, params)
|
|
137
|
+
return this.whereExpr(expr, params)
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
/**
|
|
136
|
-
* Add an IN clause
|
|
137
|
-
* Replaces any existing conditions for the same column.
|
|
141
|
+
* Add an IN clause
|
|
138
142
|
*
|
|
139
|
-
* @
|
|
140
|
-
*
|
|
141
|
-
*
|
|
143
|
+
* @example
|
|
144
|
+
* .whereIn("status", ["active", "pending"])
|
|
145
|
+
* // WHERE "status" IN (?, ?)
|
|
142
146
|
*/
|
|
143
147
|
whereIn(column: keyof T, values: SQLQueryBindings[]): this {
|
|
144
148
|
if (!Array.isArray(values) || values.length === 0) {
|
|
145
|
-
throw new Error("whereIn: values must be a non-empty array")
|
|
149
|
+
throw new Error("whereIn: values must be a non-empty array")
|
|
146
150
|
}
|
|
147
151
|
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
this.removeExistingCondition(String(column), "IN")
|
|
153
|
+
|
|
154
|
+
const { sql, params } = buildInClause(String(column), values, false)
|
|
155
|
+
this.state.whereConditions.push(sql)
|
|
156
|
+
this.state.whereParams.push(...params)
|
|
150
157
|
|
|
151
|
-
|
|
152
|
-
this.state.whereConditions.push(`${String(column)} IN (${placeholders})`);
|
|
153
|
-
this.state.whereParams.push(...values);
|
|
154
|
-
return this;
|
|
158
|
+
return this
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
/**
|
|
158
|
-
* Add a NOT IN clause
|
|
159
|
-
* Replaces any existing conditions for the same column.
|
|
162
|
+
* Add a NOT IN clause
|
|
160
163
|
*
|
|
161
|
-
* @
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
+
* @example
|
|
165
|
+
* .whereNotIn("role", ["banned", "suspended"])
|
|
166
|
+
* // WHERE "role" NOT IN (?, ?)
|
|
164
167
|
*/
|
|
165
168
|
whereNotIn(column: keyof T, values: SQLQueryBindings[]): this {
|
|
166
169
|
if (!Array.isArray(values) || values.length === 0) {
|
|
167
|
-
throw new Error("whereNotIn: values must be a non-empty array")
|
|
170
|
+
throw new Error("whereNotIn: values must be a non-empty array")
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
|
|
171
|
-
this.removeExistingCondition(String(column), "NOT IN");
|
|
173
|
+
this.removeExistingCondition(String(column), "NOT IN")
|
|
172
174
|
|
|
173
|
-
const
|
|
174
|
-
this.state.whereConditions.push(
|
|
175
|
-
this.state.whereParams.push(...
|
|
176
|
-
|
|
175
|
+
const { sql, params } = buildInClause(String(column), values, true)
|
|
176
|
+
this.state.whereConditions.push(sql)
|
|
177
|
+
this.state.whereParams.push(...params)
|
|
178
|
+
|
|
179
|
+
return this
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
/**
|
|
180
|
-
* Add a comparison operator condition
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
+
* Add a comparison operator condition
|
|
184
|
+
*
|
|
185
|
+
* Supported operators: =, !=, <>, <, <=, >, >=, LIKE, GLOB, IS, IS NOT
|
|
183
186
|
*
|
|
184
|
-
* @
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* @returns this for method chaining
|
|
187
|
+
* @example
|
|
188
|
+
* .whereOp("age", ">=", 18)
|
|
189
|
+
* .whereOp("name", "LIKE", "%smith%")
|
|
188
190
|
*/
|
|
189
191
|
whereOp(column: keyof T, op: string, value: SQLQueryBindings): this {
|
|
190
|
-
const normalizedOp = (op
|
|
191
|
-
const
|
|
192
|
-
"=",
|
|
193
|
-
"!=",
|
|
194
|
-
"<>",
|
|
195
|
-
"<",
|
|
196
|
-
"<=",
|
|
197
|
-
">",
|
|
198
|
-
">=",
|
|
199
|
-
"LIKE",
|
|
200
|
-
"GLOB",
|
|
201
|
-
"IS",
|
|
202
|
-
"IS NOT",
|
|
203
|
-
];
|
|
204
|
-
|
|
205
|
-
if (!allowed.includes(normalizedOp)) {
|
|
206
|
-
throw new Error(`whereOp: operator "${op}" not supported`);
|
|
207
|
-
}
|
|
192
|
+
const normalizedOp = normalizeOperator(op)
|
|
193
|
+
const columnStr = String(column)
|
|
208
194
|
|
|
209
|
-
// Handle
|
|
210
|
-
if (
|
|
211
|
-
(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
195
|
+
// Handle NULL special cases
|
|
196
|
+
if (value === null || value === undefined) {
|
|
197
|
+
if (normalizedOp === "=" || normalizedOp === "IS") {
|
|
198
|
+
this.state.whereConditions.push(`${quoteIdentifier(columnStr)} IS NULL`)
|
|
199
|
+
return this
|
|
200
|
+
}
|
|
201
|
+
if (normalizedOp === "!=" || normalizedOp === "<>" || normalizedOp === "IS NOT") {
|
|
202
|
+
this.state.whereConditions.push(`${quoteIdentifier(columnStr)} IS NOT NULL`)
|
|
203
|
+
return this
|
|
204
|
+
}
|
|
216
205
|
}
|
|
217
206
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
(normalizedOp === "!=" ||
|
|
221
|
-
normalizedOp === "<>" ||
|
|
222
|
-
normalizedOp === "IS NOT")
|
|
223
|
-
) {
|
|
224
|
-
this.state.whereConditions.push(`${String(column)} IS NOT NULL`);
|
|
225
|
-
return this;
|
|
226
|
-
}
|
|
207
|
+
this.state.whereConditions.push(`${quoteIdentifier(columnStr)} ${normalizedOp} ?`)
|
|
208
|
+
this.state.whereParams.push(value)
|
|
227
209
|
|
|
228
|
-
|
|
229
|
-
this.state.whereConditions.push(`${String(column)} ${normalizedOp} ?`);
|
|
230
|
-
this.state.whereParams.push(value);
|
|
231
|
-
return this;
|
|
210
|
+
return this
|
|
232
211
|
}
|
|
233
212
|
|
|
234
213
|
/**
|
|
235
|
-
* Add a BETWEEN
|
|
236
|
-
* Replaces any existing conditions for the same column.
|
|
214
|
+
* Add a BETWEEN clause
|
|
237
215
|
*
|
|
238
|
-
* @
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* @returns this for method chaining
|
|
216
|
+
* @example
|
|
217
|
+
* .whereBetween("age", 18, 65)
|
|
218
|
+
* // WHERE "age" BETWEEN ? AND ?
|
|
242
219
|
*/
|
|
243
|
-
whereBetween(
|
|
244
|
-
column
|
|
245
|
-
|
|
246
|
-
max
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.state.whereConditions.push(`${String(column)} BETWEEN ? AND ?`);
|
|
252
|
-
this.state.whereParams.push(min, max);
|
|
253
|
-
return this;
|
|
220
|
+
whereBetween(column: keyof T, min: SQLQueryBindings, max: SQLQueryBindings): this {
|
|
221
|
+
this.removeExistingCondition(String(column), "BETWEEN")
|
|
222
|
+
|
|
223
|
+
const { sql, params } = buildBetweenClause(String(column), min, max, false)
|
|
224
|
+
this.state.whereConditions.push(sql)
|
|
225
|
+
this.state.whereParams.push(...params)
|
|
226
|
+
|
|
227
|
+
return this
|
|
254
228
|
}
|
|
255
229
|
|
|
256
230
|
/**
|
|
257
|
-
* Add a NOT BETWEEN
|
|
258
|
-
* Replaces any existing conditions for the same column.
|
|
231
|
+
* Add a NOT BETWEEN clause
|
|
259
232
|
*
|
|
260
|
-
* @
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* @returns this for method chaining
|
|
233
|
+
* @example
|
|
234
|
+
* .whereNotBetween("score", 0, 50)
|
|
235
|
+
* // WHERE "score" NOT BETWEEN ? AND ?
|
|
264
236
|
*/
|
|
265
|
-
whereNotBetween(
|
|
266
|
-
column
|
|
267
|
-
|
|
268
|
-
max
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
this.state.whereConditions.push(`${String(column)} NOT BETWEEN ? AND ?`);
|
|
274
|
-
this.state.whereParams.push(min, max);
|
|
275
|
-
return this;
|
|
237
|
+
whereNotBetween(column: keyof T, min: SQLQueryBindings, max: SQLQueryBindings): this {
|
|
238
|
+
this.removeExistingCondition(String(column), "NOT BETWEEN")
|
|
239
|
+
|
|
240
|
+
const { sql, params } = buildBetweenClause(String(column), min, max, true)
|
|
241
|
+
this.state.whereConditions.push(sql)
|
|
242
|
+
this.state.whereParams.push(...params)
|
|
243
|
+
|
|
244
|
+
return this
|
|
276
245
|
}
|
|
277
246
|
|
|
278
247
|
/**
|
|
279
|
-
* Add an IS NULL condition
|
|
280
|
-
* Replaces any existing conditions for the same column.
|
|
248
|
+
* Add an IS NULL condition
|
|
281
249
|
*
|
|
282
|
-
* @
|
|
283
|
-
*
|
|
250
|
+
* @example
|
|
251
|
+
* .whereNull("deleted_at")
|
|
252
|
+
* // WHERE "deleted_at" IS NULL
|
|
284
253
|
*/
|
|
285
254
|
whereNull(column: keyof T): this {
|
|
286
|
-
|
|
287
|
-
this.
|
|
288
|
-
|
|
289
|
-
this.state.whereConditions.push(`${String(column)} IS NULL`);
|
|
290
|
-
return this;
|
|
255
|
+
this.removeExistingCondition(String(column))
|
|
256
|
+
this.state.whereConditions.push(`${quoteIdentifier(String(column))} IS NULL`)
|
|
257
|
+
return this
|
|
291
258
|
}
|
|
292
259
|
|
|
293
260
|
/**
|
|
294
|
-
* Add an IS NOT NULL condition
|
|
295
|
-
* Replaces any existing conditions for the same column.
|
|
261
|
+
* Add an IS NOT NULL condition
|
|
296
262
|
*
|
|
297
|
-
* @
|
|
298
|
-
*
|
|
263
|
+
* @example
|
|
264
|
+
* .whereNotNull("email")
|
|
265
|
+
* // WHERE "email" IS NOT NULL
|
|
299
266
|
*/
|
|
300
267
|
whereNotNull(column: keyof T): this {
|
|
301
|
-
|
|
302
|
-
this.
|
|
303
|
-
|
|
304
|
-
this.state.whereConditions.push(`${String(column)} IS NOT NULL`);
|
|
305
|
-
return this;
|
|
268
|
+
this.removeExistingCondition(String(column))
|
|
269
|
+
this.state.whereConditions.push(`${quoteIdentifier(String(column))} IS NOT NULL`)
|
|
270
|
+
return this
|
|
306
271
|
}
|
|
307
272
|
}
|