@dockstat/sqlite-wrapper 1.2.8 → 1.3.1

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.
@@ -0,0 +1,259 @@
1
+ import type { SQLQueryBindings } from "bun:sqlite"
2
+ import type { Parser } from "../types"
3
+ import { createLogger, type SqliteLogger } from "./logger"
4
+
5
+ /**
6
+ * Row Transformer for sqlite-wrapper
7
+ *
8
+ * Handles serialization (to DB) and deserialization (from DB) of row data,
9
+ * including JSON columns, Boolean columns, and Module columns.
10
+ */
11
+
12
+ const defaultLogger = createLogger("Transformer")
13
+
14
+ /**
15
+ * Generic row data type
16
+ */
17
+ export type RowData = Record<string, SQLQueryBindings>
18
+
19
+ /**
20
+ * Transform options
21
+ */
22
+ export interface TransformOptions<T> {
23
+ parser?: Parser<T>
24
+ logger?: SqliteLogger
25
+ }
26
+
27
+ /**
28
+ * Transform a row FROM the database (deserialization)
29
+ *
30
+ * - JSON columns: Parse JSON strings back to objects
31
+ * - BOOLEAN columns: Convert 0/1 to true/false
32
+ * - MODULE columns: Transpile and create importable URLs
33
+ */
34
+ export function transformFromDb<T extends Record<string, unknown>>(
35
+ row: unknown,
36
+ options?: TransformOptions<T>
37
+ ): T {
38
+ if (!row || typeof row !== "object") {
39
+ return row as T
40
+ }
41
+
42
+ const parser = options?.parser
43
+ if (!parser) {
44
+ return row as T
45
+ }
46
+
47
+ const logger = options?.logger || defaultLogger
48
+ const transformed = { ...row } as RowData
49
+ const transformedColumns: string[] = []
50
+
51
+ // Transform JSON columns
52
+ if (parser.JSON && parser.JSON.length > 0) {
53
+ for (const column of parser.JSON) {
54
+ const columnKey = String(column)
55
+ const value = transformed[columnKey]
56
+
57
+ if (value !== null && value !== undefined && typeof value === "string") {
58
+ try {
59
+ transformed[columnKey] = JSON.parse(value)
60
+ transformedColumns.push(`JSON:${columnKey}`)
61
+ } catch {
62
+ // Keep original value if JSON parsing fails
63
+ logger.warn(`Failed to parse JSON column: ${columnKey}`)
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ // Transform BOOLEAN columns
70
+ if (parser.BOOLEAN && parser.BOOLEAN.length > 0) {
71
+ for (const column of parser.BOOLEAN) {
72
+ const columnKey = String(column)
73
+ const value = transformed[columnKey]
74
+
75
+ if (value === null || value === undefined) {
76
+ continue
77
+ }
78
+
79
+ // Already a boolean - no transformation needed
80
+ if (typeof value === "boolean") {
81
+ continue
82
+ }
83
+
84
+ // Convert number (0/1) to boolean
85
+ if (typeof value === "number") {
86
+ transformed[columnKey] = value === 1
87
+ transformedColumns.push(`BOOL:${columnKey}`)
88
+ continue
89
+ }
90
+
91
+ // Convert string representations
92
+ if (typeof value === "string") {
93
+ const normalized = value.trim().toLowerCase()
94
+ if (["1", "true", "t", "yes"].includes(normalized)) {
95
+ transformed[columnKey] = true
96
+ transformedColumns.push(`BOOL:${columnKey}`)
97
+ } else if (["0", "false", "f", "no"].includes(normalized)) {
98
+ transformed[columnKey] = false
99
+ transformedColumns.push(`BOOL:${columnKey}`)
100
+ } else {
101
+ // Try numeric conversion
102
+ const num = Number(normalized)
103
+ if (!Number.isNaN(num)) {
104
+ transformed[columnKey] = num === 1
105
+ transformedColumns.push(`BOOL:${columnKey}`)
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ // Transform MODULE columns
113
+ if (parser.MODULE && Object.keys(parser.MODULE).length > 0) {
114
+ for (const [funcKey, options] of Object.entries(parser.MODULE)) {
115
+ const value = transformed[funcKey]
116
+
117
+ if (value !== undefined && value !== null && typeof value === "string") {
118
+ try {
119
+ const transpiler = new Bun.Transpiler(options)
120
+ const compiled = transpiler.transformSync(value)
121
+ const blob = new Blob([compiled], { type: "text/javascript" })
122
+ transformed[funcKey] = URL.createObjectURL(blob)
123
+ transformedColumns.push(`MODULE:${funcKey}`)
124
+ } catch (_error) {
125
+ logger.warn(`Failed to transpile MODULE column: ${funcKey}`)
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ if (transformedColumns.length > 0) {
132
+ logger.transform("deserialize", transformedColumns)
133
+ }
134
+
135
+ return transformed as T
136
+ }
137
+
138
+ /**
139
+ * Transform multiple rows FROM the database
140
+ */
141
+ export function transformRowsFromDb<T extends Record<string, unknown>>(
142
+ rows: unknown[],
143
+ options?: TransformOptions<T>
144
+ ): T[] {
145
+ if (!rows || !Array.isArray(rows)) {
146
+ return []
147
+ }
148
+
149
+ return rows.map((row) => transformFromDb<T>(row, options))
150
+ }
151
+
152
+ /**
153
+ * Transform a row TO the database (serialization)
154
+ *
155
+ * - JSON columns: Stringify objects to JSON strings
156
+ * - MODULE columns: Stringify functions
157
+ */
158
+ export function transformToDb<T extends Record<string, unknown>>(
159
+ row: Partial<T>,
160
+ options?: TransformOptions<T>
161
+ ): RowData {
162
+ if (!row || typeof row !== "object") {
163
+ return row as RowData
164
+ }
165
+
166
+ const parser = options?.parser
167
+ if (!parser) {
168
+ return row as RowData
169
+ }
170
+
171
+ const logger = options?.logger || defaultLogger
172
+ const transformed = { ...row } as RowData
173
+ const transformedColumns: string[] = []
174
+
175
+ // Serialize JSON columns
176
+ if (parser.JSON && parser.JSON.length > 0) {
177
+ for (const column of parser.JSON) {
178
+ const columnKey = String(column)
179
+ const value = transformed[columnKey]
180
+
181
+ if (value !== undefined && value !== null && typeof value === "object") {
182
+ transformed[columnKey] = JSON.stringify(value)
183
+ transformedColumns.push(`JSON:${columnKey}`)
184
+ }
185
+ }
186
+ }
187
+
188
+ // Serialize MODULE columns (functions to strings)
189
+ if (parser.MODULE && Object.keys(parser.MODULE).length > 0) {
190
+ for (const [funcKey, options] of Object.entries(parser.MODULE)) {
191
+ const value = transformed[funcKey]
192
+
193
+ if (value !== undefined && value !== null && typeof value === "function") {
194
+ try {
195
+ const transpiler = new Bun.Transpiler(options)
196
+ const fnValue = value as () => unknown
197
+ transformed[funcKey] = transpiler.transformSync(fnValue.toString())
198
+ transformedColumns.push(`MODULE:${funcKey}`)
199
+ } catch {
200
+ logger.warn(`Failed to serialize MODULE column: ${funcKey}`)
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ if (transformedColumns.length > 0) {
207
+ logger.transform("serialize", transformedColumns)
208
+ }
209
+
210
+ return transformed
211
+ }
212
+
213
+ /**
214
+ * Transform multiple rows TO the database
215
+ */
216
+ export function transformRowsToDb<T extends Record<string, unknown>>(
217
+ rows: Partial<T>[],
218
+ options?: TransformOptions<T>
219
+ ): RowData[] {
220
+ if (!rows || !Array.isArray(rows)) {
221
+ return []
222
+ }
223
+
224
+ return rows.map((row) => transformToDb<T>(row, options))
225
+ }
226
+
227
+ /**
228
+ * Check if a parser has any transformations configured
229
+ */
230
+ export function hasTransformations<T>(parser?: Parser<T>): boolean {
231
+ if (!parser) return false
232
+
233
+ const hasJson = !!(parser.JSON && parser.JSON.length > 0)
234
+ const hasBoolean = !!(parser.BOOLEAN && parser.BOOLEAN.length > 0)
235
+ const hasModule = !!(parser.MODULE && Object.keys(parser.MODULE).length > 0)
236
+
237
+ return hasJson || hasBoolean || hasModule
238
+ }
239
+
240
+ /**
241
+ * Get a summary of parser configuration
242
+ */
243
+ export function getParserSummary<T>(parser?: Parser<T>): string {
244
+ if (!parser) return "none"
245
+
246
+ const parts: string[] = []
247
+
248
+ if (parser.JSON && parser.JSON.length > 0) {
249
+ parts.push(`JSON(${parser.JSON.length})`)
250
+ }
251
+ if (parser.BOOLEAN && parser.BOOLEAN.length > 0) {
252
+ parts.push(`BOOL(${parser.BOOLEAN.length})`)
253
+ }
254
+ if (parser.MODULE && Object.keys(parser.MODULE).length > 0) {
255
+ parts.push(`MODULE(${Object.keys(parser.MODULE).length})`)
256
+ }
257
+
258
+ return parts.length > 0 ? parts.join(", ") : "none"
259
+ }