@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/package.json CHANGED
@@ -1,13 +1,19 @@
1
1
  {
2
2
  "name": "@dockstat/sqlite-wrapper",
3
- "version": "1.2.7",
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 run test.ts"
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": "^1.0.0"
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.42"
58
+ "@types/dockerode": "^3.3.44"
53
59
  }
54
60
  }
@@ -1,58 +1,42 @@
1
- import type { Database, SQLQueryBindings } from 'bun:sqlite'
2
- import { createLogger } from '@dockstat/logger'
3
- import type {
4
- ColumnNames,
5
- DatabaseRowData,
6
- JsonColumnConfig,
7
- OrderDirection,
8
- QueryBuilderState,
9
- } from '../types'
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
- * This class provides the foundation for all query operations.
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
- jsonColumns: jsonConfig,
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
- * Build the WHERE clause portion of a SQL query.
72
- * @returns Tuple of [whereClause, parameters] where whereClause includes "WHERE" prefix
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
- return [
79
- ` WHERE ${this.state.whereConditions.join(' AND ')}`,
80
- this.state.whereParams.slice(),
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 that require client-side filtering.
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 client-side regex filtering to a set of rows.
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.state.regexConditions.length === 0) {
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 that require them.
111
- * Throws an error if no WHERE conditions are present.
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
- if (
115
- this.state.whereConditions.length === 0 &&
116
- this.state.regexConditions.length === 0
117
- ) {
118
- const error = `${operation} operation requires at least one WHERE condition. Use where(), whereRaw(), whereIn(), whereOp(), or whereRgx() to add conditions.`
119
- this.logger.error(error)
120
- throw new Error(error)
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 row data after fetching from database (deserialize JSON columns).
162
+ * Transform a single row FROM the database
163
+ * (deserialize JSON, convert booleans, etc.)
143
164
  */
144
165
  protected transformRowFromDb(row: unknown): T {
145
- if (!this.state.jsonColumns || !row) return row as T
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 after fetching from database.
170
+ * Transform multiple rows FROM the database
177
171
  */
178
172
  protected transformRowsFromDb(rows: unknown[]): T[] {
179
- if (!this.state.jsonColumns || !Array.isArray(rows)) return rows as T[]
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 data before inserting/updating to database (serialize JSON columns).
177
+ * Transform a row TO the database
178
+ * (serialize JSON, stringify functions, etc.)
198
179
  */
199
- protected transformRowToDb(row: Partial<T>): DatabaseRowData {
200
- this.logger.debug(`Transforming row (${JSON.stringify(row)}) to row Data`)
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
  }