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