@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/utils/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for sqlite-wrapper
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all utility modules for easy importing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Logger utilities
|
|
8
|
+
export {
|
|
9
|
+
addLoggerParents,
|
|
10
|
+
createLogger,
|
|
11
|
+
logger,
|
|
12
|
+
SqliteLogger,
|
|
13
|
+
} from "./logger"
|
|
14
|
+
|
|
15
|
+
// SQL utilities
|
|
16
|
+
export {
|
|
17
|
+
buildBetweenClause,
|
|
18
|
+
buildCondition,
|
|
19
|
+
buildDeleteSQL,
|
|
20
|
+
buildInClause,
|
|
21
|
+
buildInsertSQL,
|
|
22
|
+
buildPlaceholders,
|
|
23
|
+
buildSelectSQL,
|
|
24
|
+
buildSetClause,
|
|
25
|
+
buildUpdateSQL,
|
|
26
|
+
escapeValue,
|
|
27
|
+
isSQLFunction,
|
|
28
|
+
normalizeOperator,
|
|
29
|
+
quoteIdentifier,
|
|
30
|
+
quoteIdentifiers,
|
|
31
|
+
quoteString,
|
|
32
|
+
} from "./sql"
|
|
33
|
+
|
|
34
|
+
// Transformer utilities
|
|
35
|
+
export {
|
|
36
|
+
getParserSummary,
|
|
37
|
+
hasTransformations,
|
|
38
|
+
type RowData,
|
|
39
|
+
type TransformOptions,
|
|
40
|
+
transformFromDb,
|
|
41
|
+
transformRowsFromDb,
|
|
42
|
+
transformRowsToDb,
|
|
43
|
+
transformToDb,
|
|
44
|
+
} from "./transformer"
|
package/utils/logger.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import Logger from "@dockstat/logger"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Centralized logging for sqlite-wrapper
|
|
5
|
+
*
|
|
6
|
+
* This module provides a clean, consistent logging interface
|
|
7
|
+
* for all sqlite-wrapper operations.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Truncate a string to a maximum length with ellipsis
|
|
12
|
+
*/
|
|
13
|
+
function truncate(str: string, maxLength: number): string {
|
|
14
|
+
if (str.length <= maxLength) return str
|
|
15
|
+
return `${str.slice(0, maxLength)}...`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format query parameters for logging (truncated for readability)
|
|
20
|
+
*/
|
|
21
|
+
function formatParams(params?: unknown[]): string {
|
|
22
|
+
if (!params || params.length === 0) return ""
|
|
23
|
+
const str = JSON.stringify(params)
|
|
24
|
+
return truncate(str, 60)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SqliteLogger - A wrapper around @dockstat/logger with sqlite-specific helpers
|
|
29
|
+
*/
|
|
30
|
+
export class SqliteLogger {
|
|
31
|
+
private logger: Logger
|
|
32
|
+
private tableName?: string
|
|
33
|
+
|
|
34
|
+
constructor(name: string, parent?: Logger, tableName?: string) {
|
|
35
|
+
this.logger = parent ? parent.spawn(name) : new Logger(name)
|
|
36
|
+
this.tableName = tableName
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a child logger for a specific component
|
|
41
|
+
*/
|
|
42
|
+
child(name: string): SqliteLogger {
|
|
43
|
+
return new SqliteLogger(name, this.logger, this.tableName)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a table-scoped logger
|
|
48
|
+
*/
|
|
49
|
+
forTable(tableName: string): SqliteLogger {
|
|
50
|
+
const child = new SqliteLogger(tableName, this.logger, tableName)
|
|
51
|
+
return child
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ===== Standard log methods =====
|
|
55
|
+
|
|
56
|
+
debug(message: string): void {
|
|
57
|
+
this.logger.debug(message)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
info(message: string): void {
|
|
61
|
+
this.logger.info(message)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
warn(message: string): void {
|
|
65
|
+
this.logger.warn(message)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
error(message: string): void {
|
|
69
|
+
this.logger.error(message)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ===== Sqlite-specific log helpers =====
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Log a database connection event
|
|
76
|
+
*/
|
|
77
|
+
connection(path: string, action: "open" | "close"): void {
|
|
78
|
+
this.logger.info(`Database ${action}: ${path}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Log a SQL query execution
|
|
83
|
+
*/
|
|
84
|
+
query(operation: string, sql: string, params?: unknown[]): void {
|
|
85
|
+
const paramStr = formatParams(params)
|
|
86
|
+
const sqlStr = truncate(sql.replace(/\s+/g, " ").trim(), 100)
|
|
87
|
+
const msg = paramStr
|
|
88
|
+
? `${operation} | ${sqlStr} | params=${paramStr}`
|
|
89
|
+
: `${operation} | ${sqlStr}`
|
|
90
|
+
this.logger.debug(msg)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Log query results
|
|
95
|
+
*/
|
|
96
|
+
result(operation: string, rowCount: number): void {
|
|
97
|
+
this.logger.debug(`${operation} | rows=${rowCount}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Log a table creation
|
|
102
|
+
*/
|
|
103
|
+
tableCreate(tableName: string, columns: string[]): void {
|
|
104
|
+
this.logger.debug(`CREATE TABLE ${tableName} | columns=[${columns.join(", ")}]`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Log backup operations
|
|
109
|
+
*/
|
|
110
|
+
backup(action: "create" | "restore" | "list" | "delete", path?: string): void {
|
|
111
|
+
const msg = path ? `Backup ${action}: ${path}` : `Backup ${action}`
|
|
112
|
+
this.logger.info(msg)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Log row transformation
|
|
117
|
+
*/
|
|
118
|
+
transform(direction: "serialize" | "deserialize", columnTypes: string[]): void {
|
|
119
|
+
if (columnTypes.length === 0) return
|
|
120
|
+
this.logger.debug(`Transform ${direction}: [${columnTypes.join(", ")}]`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Log parser configuration
|
|
125
|
+
*/
|
|
126
|
+
parserConfig(json: string[], boolean: string[], module: string[]): void {
|
|
127
|
+
const parts: string[] = []
|
|
128
|
+
if (json.length > 0) parts.push(`JSON=[${json.join(",")}]`)
|
|
129
|
+
if (boolean.length > 0) parts.push(`BOOL=[${boolean.join(",")}]`)
|
|
130
|
+
if (module.length > 0) parts.push(`MODULE=[${module.join(",")}]`)
|
|
131
|
+
if (parts.length > 0) {
|
|
132
|
+
this.logger.debug(`Parser config: ${parts.join(" ")}`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Log transaction events
|
|
138
|
+
*/
|
|
139
|
+
transaction(action: "begin" | "commit" | "rollback" | "savepoint", name?: string): void {
|
|
140
|
+
const msg = name ? `Transaction ${action}: ${name}` : `Transaction ${action}`
|
|
141
|
+
this.logger.debug(msg)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the underlying @dockstat/logger instance
|
|
146
|
+
*/
|
|
147
|
+
getBaseLogger(): Logger {
|
|
148
|
+
return this.logger
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Add parent loggers for chaining
|
|
153
|
+
*/
|
|
154
|
+
addParents(parents: string[]): void {
|
|
155
|
+
this.logger.addParents(parents)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get parents for logger chaining
|
|
160
|
+
*/
|
|
161
|
+
getParentsForLoggerChaining(): string[] {
|
|
162
|
+
return this.logger.getParentsForLoggerChaining()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ===== Module exports =====
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Main logger instance for sqlite-wrapper
|
|
170
|
+
*/
|
|
171
|
+
export const logger = new SqliteLogger("Sqlite")
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Create a new logger for a specific module
|
|
175
|
+
*/
|
|
176
|
+
export function createLogger(name: string): SqliteLogger {
|
|
177
|
+
return logger.child(name)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function addLoggerParents(parents: string[]): void {
|
|
181
|
+
logger.addParents(parents)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export default logger
|
package/utils/sql.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { SQLQueryBindings } from "bun:sqlite"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SQL Utilities for sqlite-wrapper
|
|
5
|
+
*
|
|
6
|
+
* Common SQL operations and helpers used across the package.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Quote a SQL identifier (table name, column name) to prevent injection
|
|
11
|
+
* and handle special characters.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* quoteIdentifier("users") // "users"
|
|
15
|
+
* quoteIdentifier("user name") // "user name"
|
|
16
|
+
* quoteIdentifier('with"quote') // "with""quote"
|
|
17
|
+
*/
|
|
18
|
+
export function quoteIdentifier(identifier: string): string {
|
|
19
|
+
return `"${identifier.replace(/"/g, '""')}"`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Quote a SQL string literal value
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* quoteString("hello") // 'hello'
|
|
27
|
+
* quoteString("it's") // 'it''s'
|
|
28
|
+
*/
|
|
29
|
+
export function quoteString(value: string): string {
|
|
30
|
+
return `'${value.replace(/'/g, "''")}'`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a comma-separated list of quoted identifiers
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* quoteIdentifiers(["id", "name", "email"]) // "id", "name", "email"
|
|
38
|
+
*/
|
|
39
|
+
export function quoteIdentifiers(identifiers: string[]): string {
|
|
40
|
+
return identifiers.map(quoteIdentifier).join(", ")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build placeholders for parameterized queries
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* buildPlaceholders(3) // "?, ?, ?"
|
|
48
|
+
* buildPlaceholders(["a", "b"]) // "?, ?"
|
|
49
|
+
*/
|
|
50
|
+
export function buildPlaceholders(countOrArray: number | unknown[]): string {
|
|
51
|
+
const count = typeof countOrArray === "number" ? countOrArray : countOrArray.length
|
|
52
|
+
return Array(count).fill("?").join(", ")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build a SET clause for UPDATE statements
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* buildSetClause(["name", "email"]) // "name" = ?, "email" = ?
|
|
60
|
+
*/
|
|
61
|
+
export function buildSetClause(columns: string[]): string {
|
|
62
|
+
return columns.map((col) => `${quoteIdentifier(col)} = ?`).join(", ")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build an INSERT statement
|
|
67
|
+
*/
|
|
68
|
+
export function buildInsertSQL(
|
|
69
|
+
table: string,
|
|
70
|
+
columns: string[],
|
|
71
|
+
conflictResolution?: "IGNORE" | "REPLACE" | "ABORT" | "FAIL" | "ROLLBACK"
|
|
72
|
+
): string {
|
|
73
|
+
const insertType = conflictResolution ? `INSERT OR ${conflictResolution}` : "INSERT"
|
|
74
|
+
const quotedColumns = quoteIdentifiers(columns)
|
|
75
|
+
const placeholders = buildPlaceholders(columns)
|
|
76
|
+
|
|
77
|
+
return `${insertType} INTO ${quoteIdentifier(table)} (${quotedColumns}) VALUES (${placeholders})`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build a simple SELECT statement
|
|
82
|
+
*/
|
|
83
|
+
export function buildSelectSQL(
|
|
84
|
+
table: string,
|
|
85
|
+
columns: string[] | "*",
|
|
86
|
+
options?: {
|
|
87
|
+
where?: string
|
|
88
|
+
orderBy?: string
|
|
89
|
+
orderDirection?: "ASC" | "DESC"
|
|
90
|
+
limit?: number
|
|
91
|
+
offset?: number
|
|
92
|
+
}
|
|
93
|
+
): string {
|
|
94
|
+
const cols = columns === "*" ? "*" : quoteIdentifiers(columns)
|
|
95
|
+
let sql = `SELECT ${cols} FROM ${quoteIdentifier(table)}`
|
|
96
|
+
|
|
97
|
+
if (options?.where) {
|
|
98
|
+
sql += ` WHERE ${options.where}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (options?.orderBy) {
|
|
102
|
+
sql += ` ORDER BY ${quoteIdentifier(options.orderBy)} ${options.orderDirection || "ASC"}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (options?.limit !== undefined) {
|
|
106
|
+
sql += ` LIMIT ${options.limit}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options?.offset !== undefined) {
|
|
110
|
+
sql += ` OFFSET ${options.offset}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return sql
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build an UPDATE statement
|
|
118
|
+
*/
|
|
119
|
+
export function buildUpdateSQL(table: string, columns: string[], where: string): string {
|
|
120
|
+
const setClause = buildSetClause(columns)
|
|
121
|
+
return `UPDATE ${quoteIdentifier(table)} SET ${setClause} WHERE ${where}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build a DELETE statement
|
|
126
|
+
*/
|
|
127
|
+
export function buildDeleteSQL(table: string, where: string): string {
|
|
128
|
+
return `DELETE FROM ${quoteIdentifier(table)} WHERE ${where}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if a string looks like a SQL function call
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* isSQLFunction("datetime('now')") // true
|
|
136
|
+
* isSQLFunction("CURRENT_TIMESTAMP") // true
|
|
137
|
+
* isSQLFunction("hello") // false
|
|
138
|
+
*/
|
|
139
|
+
export function isSQLFunction(value: string): boolean {
|
|
140
|
+
const functionPatterns = [
|
|
141
|
+
/^\w+\s*\(/i, // function(...)
|
|
142
|
+
/^CURRENT_TIME(STAMP)?$/i,
|
|
143
|
+
/^CURRENT_DATE$/i,
|
|
144
|
+
/^NULL$/i,
|
|
145
|
+
]
|
|
146
|
+
return functionPatterns.some((pattern) => pattern.test(value.trim()))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Escape a value for safe inclusion in SQL
|
|
151
|
+
* Returns the SQLite-safe representation
|
|
152
|
+
*/
|
|
153
|
+
export function escapeValue(value: SQLQueryBindings): string {
|
|
154
|
+
if (value === null) return "NULL"
|
|
155
|
+
if (typeof value === "number") return String(value)
|
|
156
|
+
if (typeof value === "boolean") return value ? "1" : "0"
|
|
157
|
+
if (typeof value === "string") return quoteString(value)
|
|
158
|
+
if (value instanceof Uint8Array) return `X'${Buffer.from(value).toString("hex")}'`
|
|
159
|
+
return quoteString(String(value))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Normalize a comparison operator
|
|
164
|
+
*/
|
|
165
|
+
export function normalizeOperator(op: string): string {
|
|
166
|
+
const normalized = op.toUpperCase().trim()
|
|
167
|
+
const allowed = ["=", "!=", "<>", "<", "<=", ">", ">=", "LIKE", "GLOB", "IS", "IS NOT"]
|
|
168
|
+
|
|
169
|
+
if (!allowed.includes(normalized)) {
|
|
170
|
+
throw new Error(`Invalid SQL operator: "${op}"`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return normalized
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Build a WHERE condition for a single column
|
|
178
|
+
*/
|
|
179
|
+
export function buildCondition(
|
|
180
|
+
column: string,
|
|
181
|
+
operator: string,
|
|
182
|
+
value: SQLQueryBindings | null | undefined
|
|
183
|
+
): { sql: string; params: SQLQueryBindings[] } {
|
|
184
|
+
const normalizedOp = normalizeOperator(operator)
|
|
185
|
+
const quotedCol = quoteIdentifier(column)
|
|
186
|
+
|
|
187
|
+
// Handle NULL special cases
|
|
188
|
+
if (value === null || value === undefined) {
|
|
189
|
+
if (normalizedOp === "=" || normalizedOp === "IS") {
|
|
190
|
+
return { sql: `${quotedCol} IS NULL`, params: [] }
|
|
191
|
+
}
|
|
192
|
+
if (normalizedOp === "!=" || normalizedOp === "<>" || normalizedOp === "IS NOT") {
|
|
193
|
+
return { sql: `${quotedCol} IS NOT NULL`, params: [] }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
sql: `${quotedCol} ${normalizedOp} ?`,
|
|
199
|
+
params: [value as SQLQueryBindings],
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Build an IN clause
|
|
205
|
+
*/
|
|
206
|
+
export function buildInClause(
|
|
207
|
+
column: string,
|
|
208
|
+
values: SQLQueryBindings[],
|
|
209
|
+
negate = false
|
|
210
|
+
): { sql: string; params: SQLQueryBindings[] } {
|
|
211
|
+
if (values.length === 0) {
|
|
212
|
+
throw new Error("IN clause requires at least one value")
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const quotedCol = quoteIdentifier(column)
|
|
216
|
+
const placeholders = buildPlaceholders(values)
|
|
217
|
+
const keyword = negate ? "NOT IN" : "IN"
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
sql: `${quotedCol} ${keyword} (${placeholders})`,
|
|
221
|
+
params: values,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Build a BETWEEN clause
|
|
227
|
+
*/
|
|
228
|
+
export function buildBetweenClause(
|
|
229
|
+
column: string,
|
|
230
|
+
min: SQLQueryBindings,
|
|
231
|
+
max: SQLQueryBindings,
|
|
232
|
+
negate = false
|
|
233
|
+
): { sql: string; params: SQLQueryBindings[] } {
|
|
234
|
+
const quotedCol = quoteIdentifier(column)
|
|
235
|
+
const keyword = negate ? "NOT BETWEEN" : "BETWEEN"
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
sql: `${quotedCol} ${keyword} ? AND ?`,
|
|
239
|
+
params: [min, max],
|
|
240
|
+
}
|
|
241
|
+
}
|