sails-sqlite 0.1.0 → 0.2.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.
- package/lib/index.js +215 -28
- package/lib/private/build-std-adapter-method.js +8 -4
- package/lib/private/machines/avg-records.js +1 -1
- package/lib/private/machines/begin-transaction.js +51 -0
- package/lib/private/machines/commit-transaction.js +50 -0
- package/lib/private/machines/compile-statement.js +62 -0
- package/lib/private/machines/count-records.js +1 -1
- package/lib/private/machines/create-each-record.js +1 -1
- package/lib/private/machines/create-record.js +2 -2
- package/lib/private/machines/define-physical-model.js +18 -9
- package/lib/private/machines/destroy-records.js +21 -8
- package/lib/private/machines/drop-physical-model.js +2 -2
- package/lib/private/machines/find-records.js +3 -3
- package/lib/private/machines/join.js +232 -66
- package/lib/private/machines/lease-connection.js +58 -0
- package/lib/private/machines/parse-native-query-error.js +90 -0
- package/lib/private/machines/parse-native-query-result.js +59 -0
- package/lib/private/machines/private/build-sqlite-where-clause.js +10 -8
- package/lib/private/machines/private/compile-statement.js +382 -0
- package/lib/private/machines/private/generate-join-sql-query.js +14 -6
- package/lib/private/machines/private/process-each-record.js +21 -4
- package/lib/private/machines/private/process-native-error.js +85 -40
- package/lib/private/machines/rollback-transaction.js +50 -0
- package/lib/private/machines/send-native-query.js +100 -0
- package/lib/private/machines/sum-records.js +1 -1
- package/lib/private/machines/update-records.js +27 -10
- package/package.json +8 -3
- package/tests/index.js +1 -1
- package/tests/runner.js +99 -0
- package/tests/transaction.test.js +562 -0
- package/tests/adapter.test.js +0 -534
- package/tests/datatypes.test.js +0 -293
- package/tests/sequence.test.js +0 -153
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile a Waterline statement into a native SQLite query
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const buildSqliteWhereClause = require('./build-sqlite-where-clause')
|
|
6
|
+
|
|
7
|
+
module.exports = function compileStatement(statement) {
|
|
8
|
+
if (!statement) {
|
|
9
|
+
throw new Error('Statement is required')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let sql = ''
|
|
13
|
+
let bindings = []
|
|
14
|
+
|
|
15
|
+
// Handle UNION ALL queries
|
|
16
|
+
if (statement.unionAll && Array.isArray(statement.unionAll)) {
|
|
17
|
+
const unionQueries = []
|
|
18
|
+
let globalOrderBy = null
|
|
19
|
+
|
|
20
|
+
statement.unionAll.forEach((unionStatement) => {
|
|
21
|
+
let processedStatement = { ...unionStatement }
|
|
22
|
+
|
|
23
|
+
// Remove ORDER BY, LIMIT, SKIP from individual queries - apply globally
|
|
24
|
+
if (!globalOrderBy && unionStatement.orderBy) {
|
|
25
|
+
globalOrderBy = unionStatement.orderBy
|
|
26
|
+
}
|
|
27
|
+
delete processedStatement.orderBy
|
|
28
|
+
delete processedStatement.limit
|
|
29
|
+
delete processedStatement.skip
|
|
30
|
+
|
|
31
|
+
const compiledUnion = compileStatement(processedStatement)
|
|
32
|
+
unionQueries.push(compiledUnion.sql)
|
|
33
|
+
bindings = bindings.concat(compiledUnion.bindings || [])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
sql = unionQueries.join(' UNION ALL ')
|
|
37
|
+
|
|
38
|
+
// Apply global ORDER BY if present
|
|
39
|
+
if (
|
|
40
|
+
globalOrderBy &&
|
|
41
|
+
Array.isArray(globalOrderBy) &&
|
|
42
|
+
globalOrderBy.length > 0
|
|
43
|
+
) {
|
|
44
|
+
const orderClauses = globalOrderBy.map((orderItem) => {
|
|
45
|
+
if (typeof orderItem === 'string') {
|
|
46
|
+
if (orderItem.includes('.')) {
|
|
47
|
+
const parts = orderItem.split('.')
|
|
48
|
+
if (parts.length === 2) {
|
|
49
|
+
const [tableName, columnName] = parts
|
|
50
|
+
return `\`${tableName}\`.\`${columnName}\` ASC`
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return `\`${orderItem}\` ASC`
|
|
54
|
+
}
|
|
55
|
+
if (typeof orderItem === 'object') {
|
|
56
|
+
const key = Object.keys(orderItem)[0]
|
|
57
|
+
const direction =
|
|
58
|
+
orderItem[key].toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
|
|
59
|
+
if (key.includes('.')) {
|
|
60
|
+
const parts = key.split('.')
|
|
61
|
+
if (parts.length === 2) {
|
|
62
|
+
const [tableName, columnName] = parts
|
|
63
|
+
return `\`${tableName}\`.\`${columnName}\` ${direction}`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return `\`${key}\` ${direction}`
|
|
67
|
+
}
|
|
68
|
+
return orderItem
|
|
69
|
+
})
|
|
70
|
+
sql += ` ORDER BY ${orderClauses.join(', ')}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { sql, bindings }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle regular SELECT statements
|
|
77
|
+
if (statement.select) {
|
|
78
|
+
// Determine parent table for ORDER BY disambiguation
|
|
79
|
+
let parentTable = null
|
|
80
|
+
if (statement.from) {
|
|
81
|
+
if (statement.from.includes(' as ')) {
|
|
82
|
+
parentTable = statement.from.split(' as ')[0].trim()
|
|
83
|
+
} else {
|
|
84
|
+
parentTable = statement.from
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const hasJoins =
|
|
88
|
+
statement.leftOuterJoin && statement.leftOuterJoin.length > 0
|
|
89
|
+
|
|
90
|
+
// SELECT clause
|
|
91
|
+
if (Array.isArray(statement.select) && statement.select.length > 0) {
|
|
92
|
+
const selectColumns = statement.select.map((col) => {
|
|
93
|
+
// Handle columns with aliases (e.g., 'table.column as alias')
|
|
94
|
+
if (col.includes(' as ')) {
|
|
95
|
+
const [columnPart, aliasPart] = col.split(' as ')
|
|
96
|
+
const alias = aliasPart.trim()
|
|
97
|
+
|
|
98
|
+
// Process the column part
|
|
99
|
+
let formattedColumn
|
|
100
|
+
if (columnPart.includes('.')) {
|
|
101
|
+
const parts = columnPart.split('.')
|
|
102
|
+
if (parts.length === 2) {
|
|
103
|
+
const [tableName, columnName] = parts
|
|
104
|
+
formattedColumn = `\`${tableName}\`.\`${columnName}\``
|
|
105
|
+
} else {
|
|
106
|
+
formattedColumn = columnPart
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
formattedColumn = `\`${columnPart}\``
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `${formattedColumn} AS ${alias}`
|
|
113
|
+
}
|
|
114
|
+
// Handle table-prefixed columns (e.g., 'tableName.columnName')
|
|
115
|
+
else if (col.includes('.')) {
|
|
116
|
+
const parts = col.split('.')
|
|
117
|
+
if (parts.length === 2) {
|
|
118
|
+
const [tableName, columnName] = parts
|
|
119
|
+
return `\`${tableName}\`.\`${columnName}\``
|
|
120
|
+
}
|
|
121
|
+
// Handle complex column expressions
|
|
122
|
+
return col
|
|
123
|
+
} else {
|
|
124
|
+
return `\`${col}\``
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
sql += `SELECT ${selectColumns.join(', ')}`
|
|
128
|
+
} else {
|
|
129
|
+
sql += 'SELECT *'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// FROM clause
|
|
133
|
+
if (statement.from) {
|
|
134
|
+
// Handle table aliases (e.g., "paymentTable as paymentTable__payments")
|
|
135
|
+
if (statement.from.includes(' as ')) {
|
|
136
|
+
const [tableName, alias] = statement.from.split(' as ')
|
|
137
|
+
sql += ` FROM \`${tableName.trim()}\` AS \`${alias.trim()}\``
|
|
138
|
+
} else {
|
|
139
|
+
sql += ` FROM \`${statement.from}\``
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// JOIN clauses
|
|
144
|
+
if (statement.leftOuterJoin && Array.isArray(statement.leftOuterJoin)) {
|
|
145
|
+
statement.leftOuterJoin.forEach((join) => {
|
|
146
|
+
if (join.from && join.on) {
|
|
147
|
+
// Handle table aliases in JOIN
|
|
148
|
+
let joinTable
|
|
149
|
+
if (join.from.includes(' as ')) {
|
|
150
|
+
const [tableName, alias] = join.from.split(' as ')
|
|
151
|
+
joinTable = `\`${tableName.trim()}\` AS \`${alias.trim()}\``
|
|
152
|
+
} else {
|
|
153
|
+
joinTable = `\`${join.from}\``
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
sql += ` LEFT OUTER JOIN ${joinTable} ON `
|
|
157
|
+
|
|
158
|
+
// Build the ON conditions
|
|
159
|
+
const onConditions = []
|
|
160
|
+
Object.keys(join.on).forEach((tableName) => {
|
|
161
|
+
const columnName = join.on[tableName]
|
|
162
|
+
// The key is a table name, value is a column name
|
|
163
|
+
// We need to format as table.column for both sides
|
|
164
|
+
const formattedTableCol = `\`${tableName}\`.\`${columnName}\``
|
|
165
|
+
onConditions.push(formattedTableCol)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Join conditions should be joined with =
|
|
169
|
+
// If we have 2 conditions, it should be table1.col1 = table2.col2
|
|
170
|
+
if (onConditions.length === 2) {
|
|
171
|
+
sql += `${onConditions[0]} = ${onConditions[1]}`
|
|
172
|
+
} else {
|
|
173
|
+
// Fallback for other cases
|
|
174
|
+
sql += onConditions.join(' AND ')
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// WHERE clause
|
|
181
|
+
if (statement.where) {
|
|
182
|
+
const whereResult = buildWhereClause(statement.where)
|
|
183
|
+
if (whereResult.clause) {
|
|
184
|
+
sql += ` WHERE ${whereResult.clause}`
|
|
185
|
+
bindings = bindings.concat(whereResult.bindings || [])
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ORDER BY clause
|
|
190
|
+
if (
|
|
191
|
+
statement.orderBy &&
|
|
192
|
+
Array.isArray(statement.orderBy) &&
|
|
193
|
+
statement.orderBy.length > 0
|
|
194
|
+
) {
|
|
195
|
+
const orderClauses = statement.orderBy.map((orderItem) => {
|
|
196
|
+
if (typeof orderItem === 'string') {
|
|
197
|
+
if (orderItem.includes('.')) {
|
|
198
|
+
const parts = orderItem.split('.')
|
|
199
|
+
if (parts.length === 2) {
|
|
200
|
+
const [tableName, columnName] = parts
|
|
201
|
+
return `\`${tableName}\`.\`${columnName}\` ASC`
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// If there are JOINs and column is unqualified, prefix with parent table
|
|
205
|
+
if (hasJoins && parentTable) {
|
|
206
|
+
return `\`${parentTable}\`.\`${orderItem}\` ASC`
|
|
207
|
+
}
|
|
208
|
+
return `\`${orderItem}\` ASC`
|
|
209
|
+
}
|
|
210
|
+
if (typeof orderItem === 'object') {
|
|
211
|
+
const key = Object.keys(orderItem)[0]
|
|
212
|
+
const direction =
|
|
213
|
+
orderItem[key].toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
|
|
214
|
+
if (key.includes('.')) {
|
|
215
|
+
const parts = key.split('.')
|
|
216
|
+
if (parts.length === 2) {
|
|
217
|
+
const [tableName, columnName] = parts
|
|
218
|
+
return `\`${tableName}\`.\`${columnName}\` ${direction}`
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// If there are JOINs and column is unqualified, prefix with parent table
|
|
222
|
+
if (hasJoins && parentTable) {
|
|
223
|
+
return `\`${parentTable}\`.\`${key}\` ${direction}`
|
|
224
|
+
}
|
|
225
|
+
return `\`${key}\` ${direction}`
|
|
226
|
+
}
|
|
227
|
+
return orderItem
|
|
228
|
+
})
|
|
229
|
+
sql += ` ORDER BY ${orderClauses.join(', ')}`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// LIMIT clause
|
|
233
|
+
if (typeof statement.limit === 'number') {
|
|
234
|
+
sql += ` LIMIT ${statement.limit}`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// OFFSET clause
|
|
238
|
+
if (typeof statement.skip === 'number') {
|
|
239
|
+
sql += ` OFFSET ${statement.skip}`
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { sql, bindings }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Build WHERE clause from Waterline criteria
|
|
248
|
+
*/
|
|
249
|
+
function buildWhereClause(whereObj) {
|
|
250
|
+
if (!whereObj || typeof whereObj !== 'object') {
|
|
251
|
+
return { clause: '', bindings: [] }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const conditions = []
|
|
255
|
+
const bindings = []
|
|
256
|
+
|
|
257
|
+
// Handle AND conditions
|
|
258
|
+
if (whereObj.and && Array.isArray(whereObj.and)) {
|
|
259
|
+
const andConditions = []
|
|
260
|
+
whereObj.and.forEach((condition) => {
|
|
261
|
+
const result = buildWhereClause(condition)
|
|
262
|
+
if (result.clause) {
|
|
263
|
+
andConditions.push(result.clause)
|
|
264
|
+
bindings.push(...result.bindings)
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
if (andConditions.length > 0) {
|
|
268
|
+
conditions.push(`(${andConditions.join(' AND ')})`)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Handle OR conditions
|
|
273
|
+
if (whereObj.or && Array.isArray(whereObj.or)) {
|
|
274
|
+
const orConditions = []
|
|
275
|
+
whereObj.or.forEach((condition) => {
|
|
276
|
+
const result = buildWhereClause(condition)
|
|
277
|
+
if (result.clause) {
|
|
278
|
+
orConditions.push(result.clause)
|
|
279
|
+
bindings.push(...result.bindings)
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
if (orConditions.length > 0) {
|
|
283
|
+
conditions.push(`(${orConditions.join(' OR ')})`)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Handle field conditions
|
|
288
|
+
Object.keys(whereObj).forEach((key) => {
|
|
289
|
+
if (key === 'and' || key === 'or') {
|
|
290
|
+
return // Already handled above
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const value = whereObj[key]
|
|
294
|
+
let columnName
|
|
295
|
+
|
|
296
|
+
// Handle table.column format
|
|
297
|
+
if (key.includes('.')) {
|
|
298
|
+
const parts = key.split('.')
|
|
299
|
+
if (parts.length === 2) {
|
|
300
|
+
const [tableName, colName] = parts
|
|
301
|
+
columnName = `\`${tableName}\`.\`${colName}\``
|
|
302
|
+
} else {
|
|
303
|
+
columnName = key // fallback for complex expressions
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
columnName = `\`${key}\``
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (typeof value === 'object' && value !== null) {
|
|
310
|
+
// Handle operators
|
|
311
|
+
Object.keys(value).forEach((operator) => {
|
|
312
|
+
const operatorValue = value[operator]
|
|
313
|
+
|
|
314
|
+
switch (operator) {
|
|
315
|
+
case 'in':
|
|
316
|
+
if (Array.isArray(operatorValue) && operatorValue.length > 0) {
|
|
317
|
+
const placeholders = operatorValue.map(() => '?').join(', ')
|
|
318
|
+
conditions.push(`${columnName} IN (${placeholders})`)
|
|
319
|
+
bindings.push(...operatorValue)
|
|
320
|
+
}
|
|
321
|
+
break
|
|
322
|
+
case 'nin':
|
|
323
|
+
if (Array.isArray(operatorValue) && operatorValue.length > 0) {
|
|
324
|
+
const placeholders = operatorValue.map(() => '?').join(', ')
|
|
325
|
+
conditions.push(`${columnName} NOT IN (${placeholders})`)
|
|
326
|
+
bindings.push(...operatorValue)
|
|
327
|
+
}
|
|
328
|
+
break
|
|
329
|
+
case '>':
|
|
330
|
+
conditions.push(`${columnName} > ?`)
|
|
331
|
+
bindings.push(operatorValue)
|
|
332
|
+
break
|
|
333
|
+
case '>=':
|
|
334
|
+
conditions.push(`${columnName} >= ?`)
|
|
335
|
+
bindings.push(operatorValue)
|
|
336
|
+
break
|
|
337
|
+
case '<':
|
|
338
|
+
conditions.push(`${columnName} < ?`)
|
|
339
|
+
bindings.push(operatorValue)
|
|
340
|
+
break
|
|
341
|
+
case '<=':
|
|
342
|
+
conditions.push(`${columnName} <= ?`)
|
|
343
|
+
bindings.push(operatorValue)
|
|
344
|
+
break
|
|
345
|
+
case '!=':
|
|
346
|
+
case 'ne':
|
|
347
|
+
conditions.push(`${columnName} != ?`)
|
|
348
|
+
bindings.push(operatorValue)
|
|
349
|
+
break
|
|
350
|
+
case 'like':
|
|
351
|
+
conditions.push(`${columnName} LIKE ?`)
|
|
352
|
+
bindings.push(operatorValue)
|
|
353
|
+
break
|
|
354
|
+
case 'contains':
|
|
355
|
+
conditions.push(`${columnName} LIKE ?`)
|
|
356
|
+
bindings.push(`%${operatorValue}%`)
|
|
357
|
+
break
|
|
358
|
+
case 'startsWith':
|
|
359
|
+
conditions.push(`${columnName} LIKE ?`)
|
|
360
|
+
bindings.push(`${operatorValue}%`)
|
|
361
|
+
break
|
|
362
|
+
case 'endsWith':
|
|
363
|
+
conditions.push(`${columnName} LIKE ?`)
|
|
364
|
+
bindings.push(`%${operatorValue}`)
|
|
365
|
+
break
|
|
366
|
+
default:
|
|
367
|
+
conditions.push(`${columnName} = ?`)
|
|
368
|
+
bindings.push(operatorValue)
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
} else {
|
|
372
|
+
// Simple equality
|
|
373
|
+
conditions.push(`${columnName} = ?`)
|
|
374
|
+
bindings.push(value)
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
clause: conditions.join(' AND '),
|
|
380
|
+
bindings: bindings
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -130,7 +130,7 @@ module.exports = function generateJoinSqlQuery(joinCriteria, models, query) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// Assemble the final SQL query
|
|
133
|
-
let sql = `SELECT ${sqlParts.select.join(', ')} FROM
|
|
133
|
+
let sql = `SELECT ${sqlParts.select.join(', ')} FROM \`${sqlParts.from}\``
|
|
134
134
|
|
|
135
135
|
if (sqlParts.joins.length > 0) {
|
|
136
136
|
sql += ' ' + sqlParts.joins.join(' ')
|
|
@@ -202,10 +202,18 @@ function buildWhereClause(whereObj, tableName, model) {
|
|
|
202
202
|
}
|
|
203
203
|
} else {
|
|
204
204
|
// Handle regular field conditions
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
let fullColumnName
|
|
206
|
+
if (key.includes('.')) {
|
|
207
|
+
// If the key already contains a table reference (e.g., "customerTable._id"),
|
|
208
|
+
// use it as-is instead of prepending the table name again
|
|
209
|
+
fullColumnName = key
|
|
210
|
+
} else {
|
|
211
|
+
// Otherwise, construct the full column name normally
|
|
212
|
+
const columnName = model.attributes[key]
|
|
213
|
+
? model.attributes[key].columnName || key
|
|
214
|
+
: key
|
|
215
|
+
fullColumnName = `${tableName}.${columnName}`
|
|
216
|
+
}
|
|
209
217
|
|
|
210
218
|
if (typeof value === 'object' && value !== null) {
|
|
211
219
|
// Handle operators like >, <, !=, in, etc.
|
|
@@ -301,7 +309,7 @@ function generateSqlQuery(joinCriteria, models) {
|
|
|
301
309
|
})
|
|
302
310
|
})
|
|
303
311
|
|
|
304
|
-
sql += ` FROM
|
|
312
|
+
sql += ` FROM \`${tableName}\``
|
|
305
313
|
|
|
306
314
|
// Add join clauses
|
|
307
315
|
joins.forEach((join, index) => {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* to the format expected by Waterline. This follows performance best practices.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const _ = require('@sailshq/lodash')
|
|
8
9
|
const { eachRecordDeep } = require('waterline-utils')
|
|
9
10
|
|
|
10
11
|
module.exports = function processEachRecord(options) {
|
|
@@ -45,9 +46,20 @@ module.exports = function processEachRecord(options) {
|
|
|
45
46
|
eachRecordDeep(
|
|
46
47
|
records,
|
|
47
48
|
(record, WLModel) => {
|
|
48
|
-
|
|
49
|
+
// Guard against null/undefined WLModel or definition
|
|
50
|
+
if (!WLModel || !WLModel.definition) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Use _.each instead of Object.entries for compatibility
|
|
55
|
+
_.each(WLModel.definition, (attrDef, attrName) => {
|
|
49
56
|
const columnName = attrDef.columnName || attrName
|
|
50
57
|
|
|
58
|
+
// Skip processing if this is a populated association (it's already an object/array)
|
|
59
|
+
if (attrDef.collection || attrDef.model) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
if (columnName in record) {
|
|
52
64
|
switch (attrDef.type) {
|
|
53
65
|
case 'boolean':
|
|
@@ -59,7 +71,10 @@ module.exports = function processEachRecord(options) {
|
|
|
59
71
|
|
|
60
72
|
case 'json':
|
|
61
73
|
// SQLite stores JSON as text, so we need to parse it
|
|
62
|
-
if (
|
|
74
|
+
if (
|
|
75
|
+
record[columnName] !== null &&
|
|
76
|
+
typeof record[columnName] === 'string'
|
|
77
|
+
) {
|
|
63
78
|
try {
|
|
64
79
|
record[columnName] = JSON.parse(record[columnName])
|
|
65
80
|
} catch (e) {
|
|
@@ -73,7 +88,9 @@ module.exports = function processEachRecord(options) {
|
|
|
73
88
|
|
|
74
89
|
case 'number':
|
|
75
90
|
// Ensure numbers are actually numbers
|
|
76
|
-
record[columnName]
|
|
91
|
+
if (typeof record[columnName] !== 'number') {
|
|
92
|
+
record[columnName] = Number(record[columnName])
|
|
93
|
+
}
|
|
77
94
|
break
|
|
78
95
|
|
|
79
96
|
case 'date':
|
|
@@ -90,7 +107,7 @@ module.exports = function processEachRecord(options) {
|
|
|
90
107
|
// Add more type conversions as needed
|
|
91
108
|
}
|
|
92
109
|
}
|
|
93
|
-
}
|
|
110
|
+
})
|
|
94
111
|
},
|
|
95
112
|
true,
|
|
96
113
|
identity,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const flaverr = require('flaverr')
|
|
2
|
+
|
|
1
3
|
module.exports = function processNativeError(err) {
|
|
2
4
|
if (err.footprint !== undefined) {
|
|
3
5
|
return new Error(
|
|
@@ -5,55 +7,98 @@ module.exports = function processNativeError(err) {
|
|
|
5
7
|
)
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
//
|
|
9
|
-
const SQLITE_CONSTRAINT = 19
|
|
10
|
-
const SQLITE_BUSY = 5
|
|
11
|
-
const SQLITE_READONLY = 8
|
|
12
|
-
const SQLITE_FULL = 13
|
|
13
|
-
|
|
10
|
+
// better-sqlite3 uses string-based error codes
|
|
14
11
|
switch (err.code) {
|
|
15
|
-
case SQLITE_CONSTRAINT:
|
|
16
|
-
|
|
17
|
-
identity: 'violation',
|
|
18
|
-
keys: []
|
|
19
|
-
}
|
|
20
|
-
|
|
12
|
+
case 'SQLITE_CONSTRAINT':
|
|
13
|
+
case 'SQLITE_CONSTRAINT_UNIQUE':
|
|
21
14
|
// Check if it's a UNIQUE constraint violation
|
|
22
|
-
if (
|
|
23
|
-
err.
|
|
15
|
+
if (
|
|
16
|
+
err.code === 'SQLITE_CONSTRAINT_UNIQUE' ||
|
|
17
|
+
err.message.includes('UNIQUE constraint failed')
|
|
18
|
+
) {
|
|
19
|
+
// Extract the column name from the error message
|
|
24
20
|
const match = err.message.match(/UNIQUE constraint failed: \w+\.(\w+)/)
|
|
25
|
-
|
|
26
|
-
err.footprint.keys.push(match[1])
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
break
|
|
21
|
+
const keys = match && match[1] ? [match[1]] : []
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
return flaverr(
|
|
24
|
+
{
|
|
25
|
+
name: 'UsageError',
|
|
26
|
+
code: 'E_UNIQUE',
|
|
27
|
+
message: err.message,
|
|
28
|
+
footprint: {
|
|
29
|
+
identity: 'notUnique',
|
|
30
|
+
keys: keys
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
err
|
|
34
|
+
)
|
|
34
35
|
}
|
|
35
|
-
break
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
// Generic constraint violation
|
|
38
|
+
return flaverr(
|
|
39
|
+
{
|
|
40
|
+
name: 'UsageError',
|
|
41
|
+
code: 'E_CONSTRAINT',
|
|
42
|
+
message: err.message,
|
|
43
|
+
footprint: {
|
|
44
|
+
identity: 'violation',
|
|
45
|
+
keys: []
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
err
|
|
49
|
+
)
|
|
42
50
|
|
|
43
|
-
case
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
case 'SQLITE_BUSY':
|
|
52
|
+
return flaverr(
|
|
53
|
+
{
|
|
54
|
+
name: 'UsageError',
|
|
55
|
+
code: 'E_BUSY',
|
|
56
|
+
message: err.message,
|
|
57
|
+
footprint: {
|
|
58
|
+
identity: 'busy'
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
err
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
case 'SQLITE_READONLY':
|
|
65
|
+
return flaverr(
|
|
66
|
+
{
|
|
67
|
+
name: 'UsageError',
|
|
68
|
+
code: 'E_READONLY',
|
|
69
|
+
message: err.message,
|
|
70
|
+
footprint: {
|
|
71
|
+
identity: 'readonly'
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
err
|
|
75
|
+
)
|
|
48
76
|
|
|
49
|
-
|
|
77
|
+
case 'SQLITE_FULL':
|
|
78
|
+
return flaverr(
|
|
79
|
+
{
|
|
80
|
+
name: 'UsageError',
|
|
81
|
+
code: 'E_FULL',
|
|
82
|
+
message: err.message,
|
|
83
|
+
footprint: {
|
|
84
|
+
identity: 'full'
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
err
|
|
88
|
+
)
|
|
50
89
|
|
|
51
90
|
default:
|
|
52
|
-
// For unhandled errors,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
// For unhandled errors, return a generic error with footprint
|
|
92
|
+
return flaverr(
|
|
93
|
+
{
|
|
94
|
+
name: 'Error',
|
|
95
|
+
code: 'E_UNKNOWN',
|
|
96
|
+
message: err.message,
|
|
97
|
+
footprint: {
|
|
98
|
+
identity: 'catchall'
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
err
|
|
102
|
+
)
|
|
56
103
|
}
|
|
57
|
-
|
|
58
|
-
return err
|
|
59
104
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module dependencies
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Rollback Transaction
|
|
7
|
+
*
|
|
8
|
+
* Rollback the current database transaction on the provided connection.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
friendlyName: 'Rollback transaction',
|
|
13
|
+
|
|
14
|
+
description:
|
|
15
|
+
'Rollback the current database transaction on the provided connection.',
|
|
16
|
+
|
|
17
|
+
moreInfoUrl:
|
|
18
|
+
'https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#transactionfunction---function',
|
|
19
|
+
|
|
20
|
+
inputs: {
|
|
21
|
+
connection: {
|
|
22
|
+
description:
|
|
23
|
+
'An active database connection that was acquired from a manager.',
|
|
24
|
+
example: '===',
|
|
25
|
+
required: true
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
meta: {
|
|
29
|
+
description: 'Additional options for this query.',
|
|
30
|
+
example: '==='
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
fn: function rollbackTransaction(inputs, exits) {
|
|
35
|
+
const db = inputs.connection
|
|
36
|
+
const meta = inputs.meta || {}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (!db.inTransaction) {
|
|
40
|
+
return exits.error(new Error('No active transaction to rollback.'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
db.prepare('ROLLBACK TRANSACTION').run()
|
|
44
|
+
|
|
45
|
+
return exits.success()
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return exits.error(err)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|