@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/package.json
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dockstat/sqlite-wrapper",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A TypeScript wrapper around bun:sqlite with type-safe query building",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
7
7
|
"types": "./index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.ts",
|
|
10
|
+
"./types": "./types.ts",
|
|
11
|
+
"./utils": "./utils/index.ts"
|
|
12
|
+
},
|
|
8
13
|
"files": [
|
|
9
14
|
"index.ts",
|
|
10
15
|
"query-builder/**/*",
|
|
16
|
+
"utils/**/*",
|
|
11
17
|
"README.md",
|
|
12
18
|
"types.ts"
|
|
13
19
|
],
|
|
@@ -17,7 +23,7 @@
|
|
|
17
23
|
"lint:fix": "biome lint --write .",
|
|
18
24
|
"lint:gh": "turbo run lint -- --reporter=github",
|
|
19
25
|
"check-types": "bunx tsc --noEmit",
|
|
20
|
-
"test": "bun
|
|
26
|
+
"test": "DOCKSTAT_LOGGER_IGNORE_MESSAGES='Logger Status: active - ignoring messages:' bun test"
|
|
21
27
|
},
|
|
22
28
|
"keywords": [
|
|
23
29
|
"sqlite",
|
|
@@ -42,13 +48,13 @@
|
|
|
42
48
|
"bun": ">=1.0.0"
|
|
43
49
|
},
|
|
44
50
|
"dependencies": {
|
|
45
|
-
"@dockstat/logger": "
|
|
51
|
+
"@dockstat/logger": "1.0.1"
|
|
46
52
|
},
|
|
47
53
|
"peerDependencies": {
|
|
48
|
-
"typescript": "^5"
|
|
54
|
+
"typescript": "^5.9.3"
|
|
49
55
|
},
|
|
50
56
|
"devDependencies": {
|
|
51
57
|
"@types/bun": "latest",
|
|
52
|
-
"@types/dockerode": "^3.3.
|
|
58
|
+
"@types/dockerode": "^3.3.44"
|
|
53
59
|
}
|
|
54
60
|
}
|
package/query-builder/base.ts
CHANGED
|
@@ -1,58 +1,42 @@
|
|
|
1
|
-
import type { Database, SQLQueryBindings } from
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import type { Database, SQLQueryBindings } from "bun:sqlite"
|
|
2
|
+
import type { Parser, QueryBuilderState } from "../types"
|
|
3
|
+
import {
|
|
4
|
+
createLogger,
|
|
5
|
+
quoteIdentifier,
|
|
6
|
+
type RowData,
|
|
7
|
+
transformFromDb,
|
|
8
|
+
transformRowsFromDb,
|
|
9
|
+
transformToDb,
|
|
10
|
+
} from "../utils"
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Base QueryBuilder class that manages core state and shared functionality.
|
|
13
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* This class provides the foundation for all query operations including:
|
|
16
|
+
* - Database connection and table name management
|
|
17
|
+
* - WHERE clause building and management
|
|
18
|
+
* - Regex condition handling (client-side filtering)
|
|
19
|
+
* - Row transformation (JSON/Boolean serialization)
|
|
14
20
|
*/
|
|
15
21
|
export abstract class BaseQueryBuilder<T extends Record<string, unknown>> {
|
|
16
|
-
private logger = createLogger('BaseQueryBuilder')
|
|
17
22
|
protected state: QueryBuilderState<T>
|
|
23
|
+
protected log = createLogger("query")
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
* Get the logger instance
|
|
21
|
-
*/
|
|
22
|
-
protected getLogger() {
|
|
23
|
-
return this.logger
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
constructor(
|
|
27
|
-
db: Database,
|
|
28
|
-
tableName: string,
|
|
29
|
-
jsonConfig?: JsonColumnConfig<T>
|
|
30
|
-
) {
|
|
25
|
+
constructor(db: Database, tableName: string, parser?: Parser<T>) {
|
|
31
26
|
this.state = {
|
|
32
27
|
db,
|
|
33
28
|
tableName,
|
|
34
29
|
whereConditions: [],
|
|
35
30
|
whereParams: [],
|
|
36
31
|
regexConditions: [],
|
|
37
|
-
|
|
32
|
+
parser,
|
|
38
33
|
}
|
|
39
|
-
}
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
* Reset query builder state
|
|
43
|
-
*/
|
|
44
|
-
protected reset(): void {
|
|
45
|
-
this.state.whereConditions = []
|
|
46
|
-
this.state.whereParams = []
|
|
47
|
-
this.state.regexConditions = []
|
|
48
|
-
// Reset any ordering, limit, offset, selected columns if present
|
|
49
|
-
if ('orderColumn' in this) (this as any).orderColumn = undefined
|
|
50
|
-
if ('orderDirection' in this) (this as any).orderDirection = 'ASC'
|
|
51
|
-
if ('limitValue' in this) (this as any).limitValue = undefined
|
|
52
|
-
if ('offsetValue' in this) (this as any).offsetValue = undefined
|
|
53
|
-
if ('selectedColumns' in this) (this as any).selectedColumns = ['*']
|
|
35
|
+
this.log.debug(`QueryBuilder initialized for table: ${tableName}`)
|
|
54
36
|
}
|
|
55
37
|
|
|
38
|
+
// ===== State Accessors =====
|
|
39
|
+
|
|
56
40
|
/**
|
|
57
41
|
* Get the database instance
|
|
58
42
|
*/
|
|
@@ -68,32 +52,78 @@ export abstract class BaseQueryBuilder<T extends Record<string, unknown>> {
|
|
|
68
52
|
}
|
|
69
53
|
|
|
70
54
|
/**
|
|
71
|
-
*
|
|
72
|
-
|
|
55
|
+
* Get the parser configuration
|
|
56
|
+
*/
|
|
57
|
+
protected getParser(): Parser<T> | undefined {
|
|
58
|
+
return this.state.parser
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ===== State Management =====
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reset query builder state to initial values
|
|
65
|
+
*/
|
|
66
|
+
protected reset(): void {
|
|
67
|
+
this.state.whereConditions = []
|
|
68
|
+
this.state.whereParams = []
|
|
69
|
+
this.state.regexConditions = []
|
|
70
|
+
|
|
71
|
+
// Reset any additional state in subclasses
|
|
72
|
+
if ("orderColumn" in this) this.orderColumn = undefined
|
|
73
|
+
if ("orderDirection" in this) this.orderDirection = "ASC"
|
|
74
|
+
if ("limitValue" in this) this.limitValue = undefined
|
|
75
|
+
if ("offsetValue" in this) this.offsetValue = undefined
|
|
76
|
+
if ("selectedColumns" in this) this.selectedColumns = ["*"]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Reset only WHERE conditions (useful for reusing builder)
|
|
81
|
+
*/
|
|
82
|
+
protected resetWhereConditions(): void {
|
|
83
|
+
this.state.whereConditions = []
|
|
84
|
+
this.state.whereParams = []
|
|
85
|
+
this.state.regexConditions = []
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ===== SQL Building Helpers =====
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Quote a SQL identifier to prevent injection
|
|
92
|
+
*/
|
|
93
|
+
protected quoteIdentifier(identifier: string): string {
|
|
94
|
+
return quoteIdentifier(identifier)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build the WHERE clause from accumulated conditions
|
|
99
|
+
*
|
|
100
|
+
* @returns Tuple of [whereClause, parameters]
|
|
73
101
|
*/
|
|
74
102
|
protected buildWhereClause(): [string, SQLQueryBindings[]] {
|
|
75
103
|
if (this.state.whereConditions.length === 0) {
|
|
76
|
-
return [
|
|
104
|
+
return ["", []]
|
|
77
105
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
|
|
107
|
+
const clause = ` WHERE ${this.state.whereConditions.join(" AND ")}`
|
|
108
|
+
const params = [...this.state.whereParams]
|
|
109
|
+
|
|
110
|
+
return [clause, params]
|
|
82
111
|
}
|
|
83
112
|
|
|
113
|
+
// ===== Regex Condition Handling =====
|
|
114
|
+
|
|
84
115
|
/**
|
|
85
|
-
* Check if there are any regex conditions
|
|
116
|
+
* Check if there are any regex conditions requiring client-side filtering
|
|
86
117
|
*/
|
|
87
118
|
protected hasRegexConditions(): boolean {
|
|
88
119
|
return this.state.regexConditions.length > 0
|
|
89
120
|
}
|
|
90
121
|
|
|
91
122
|
/**
|
|
92
|
-
* Apply
|
|
93
|
-
* This is used when regex conditions are present.
|
|
123
|
+
* Apply regex filtering to rows (client-side)
|
|
94
124
|
*/
|
|
95
125
|
protected applyRegexFiltering(rows: T[]): T[] {
|
|
96
|
-
if (this.
|
|
126
|
+
if (!this.hasRegexConditions()) {
|
|
97
127
|
return rows
|
|
98
128
|
}
|
|
99
129
|
|
|
@@ -106,116 +136,48 @@ export abstract class BaseQueryBuilder<T extends Record<string, unknown>> {
|
|
|
106
136
|
)
|
|
107
137
|
}
|
|
108
138
|
|
|
139
|
+
// ===== Validation =====
|
|
140
|
+
|
|
109
141
|
/**
|
|
110
|
-
* Validate that WHERE conditions exist for operations
|
|
111
|
-
*
|
|
142
|
+
* Validate that WHERE conditions exist for destructive operations
|
|
143
|
+
*
|
|
144
|
+
* @throws Error if no WHERE conditions are present
|
|
112
145
|
*/
|
|
113
146
|
protected requireWhereClause(operation: string): void {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
147
|
+
const hasWhere = this.state.whereConditions.length > 0
|
|
148
|
+
const hasRegex = this.state.regexConditions.length > 0
|
|
149
|
+
|
|
150
|
+
if (!hasWhere && !hasRegex) {
|
|
151
|
+
const message =
|
|
152
|
+
`${operation} requires at least one WHERE condition. ` +
|
|
153
|
+
`Use where(), whereRaw(), whereIn(), whereOp(), or whereRgx().`
|
|
154
|
+
this.log.error(message)
|
|
155
|
+
throw new Error(message)
|
|
121
156
|
}
|
|
122
157
|
}
|
|
123
158
|
|
|
124
|
-
|
|
125
|
-
* Quote SQL identifiers to prevent injection and handle special characters.
|
|
126
|
-
*/
|
|
127
|
-
protected quoteIdentifier(identifier: string): string {
|
|
128
|
-
return `"${identifier.replace(/"/g, '""')}"`
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Reset all WHERE conditions and parameters.
|
|
133
|
-
* Useful for reusing the same builder instance.
|
|
134
|
-
*/
|
|
135
|
-
protected resetWhereConditions(): void {
|
|
136
|
-
this.state.whereConditions = []
|
|
137
|
-
this.state.whereParams = []
|
|
138
|
-
this.state.regexConditions = []
|
|
139
|
-
}
|
|
159
|
+
// ===== Row Transformation =====
|
|
140
160
|
|
|
141
161
|
/**
|
|
142
|
-
* Transform
|
|
162
|
+
* Transform a single row FROM the database
|
|
163
|
+
* (deserialize JSON, convert booleans, etc.)
|
|
143
164
|
*/
|
|
144
165
|
protected transformRowFromDb(row: unknown): T {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
const transformed = { ...row } as DatabaseRowData
|
|
149
|
-
for (const column of this.state.jsonColumns) {
|
|
150
|
-
const columnKey = String(column)
|
|
151
|
-
if (
|
|
152
|
-
transformed[columnKey] !== null &&
|
|
153
|
-
transformed[columnKey] !== undefined &&
|
|
154
|
-
typeof transformed[columnKey] === 'string'
|
|
155
|
-
) {
|
|
156
|
-
try {
|
|
157
|
-
transformed[columnKey] = JSON.parse(
|
|
158
|
-
transformed[columnKey] as string
|
|
159
|
-
)
|
|
160
|
-
} catch (parseError) {
|
|
161
|
-
// Keep original value if JSON parsing fails
|
|
162
|
-
this.logger.warn(
|
|
163
|
-
`JSON parse failed for column ${columnKey}: ${parseError}`
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return transformed as T
|
|
169
|
-
} catch (error) {
|
|
170
|
-
this.logger.error(`Error in transformRowFromDb: ${error}`)
|
|
171
|
-
return row as T
|
|
172
|
-
}
|
|
166
|
+
return transformFromDb<T>(row, { parser: this.state.parser })
|
|
173
167
|
}
|
|
174
168
|
|
|
175
169
|
/**
|
|
176
|
-
* Transform multiple rows
|
|
170
|
+
* Transform multiple rows FROM the database
|
|
177
171
|
*/
|
|
178
172
|
protected transformRowsFromDb(rows: unknown[]): T[] {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
return rows.map((row, index) => {
|
|
183
|
-
try {
|
|
184
|
-
return this.transformRowFromDb(row)
|
|
185
|
-
} catch (error) {
|
|
186
|
-
this.logger.error(`Error transforming row ${index}: ${error}`)
|
|
187
|
-
return row as T
|
|
188
|
-
}
|
|
189
|
-
})
|
|
190
|
-
} catch (error) {
|
|
191
|
-
this.logger.error(`Error in transformRowsFromDb: ${error}`)
|
|
192
|
-
return rows as T[]
|
|
193
|
-
}
|
|
173
|
+
return transformRowsFromDb<T>(rows, { parser: this.state.parser })
|
|
194
174
|
}
|
|
195
175
|
|
|
196
176
|
/**
|
|
197
|
-
* Transform row
|
|
177
|
+
* Transform a row TO the database
|
|
178
|
+
* (serialize JSON, stringify functions, etc.)
|
|
198
179
|
*/
|
|
199
|
-
protected transformRowToDb(row: Partial<T>):
|
|
200
|
-
|
|
201
|
-
if (!this.state.jsonColumns || !row) {
|
|
202
|
-
return row as DatabaseRowData
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const transformed: DatabaseRowData = { ...row } as DatabaseRowData
|
|
206
|
-
|
|
207
|
-
for (const column of this.state.jsonColumns) {
|
|
208
|
-
const columnKey = String(column)
|
|
209
|
-
this.logger.debug(`Checking: ${columnKey}`)
|
|
210
|
-
if (
|
|
211
|
-
transformed[columnKey] !== undefined &&
|
|
212
|
-
transformed[columnKey] !== null
|
|
213
|
-
) {
|
|
214
|
-
if (typeof transformed[columnKey] === 'object') {
|
|
215
|
-
transformed[columnKey] = JSON.stringify(transformed[columnKey])
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return transformed
|
|
180
|
+
protected transformRowToDb(row: Partial<T>): RowData {
|
|
181
|
+
return transformToDb<T>(row, { parser: this.state.parser })
|
|
220
182
|
}
|
|
221
183
|
}
|