sails-sqlite 0.0.0 → 0.2.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/.github/FUNDING.yml +1 -0
- package/.github/workflows/prettier.yml +16 -0
- package/.github/workflows/test.yml +16 -0
- package/.husky/pre-commit +1 -0
- package/.prettierrc.js +5 -0
- package/CHANGELOG.md +161 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/lib/index.js +1104 -0
- package/lib/private/build-std-adapter-method.js +69 -0
- package/lib/private/constants/connection.input.js +15 -0
- package/lib/private/constants/dry-orm.input.js +23 -0
- package/lib/private/constants/meta.input.js +14 -0
- package/lib/private/constants/not-unique.exit.js +16 -0
- package/lib/private/constants/query.input.js +15 -0
- package/lib/private/constants/table-name.input.js +12 -0
- package/lib/private/machines/avg-records.js +74 -0
- package/lib/private/machines/begin-transaction.js +51 -0
- package/lib/private/machines/commit-transaction.js +50 -0
- package/lib/private/machines/count-records.js +78 -0
- package/lib/private/machines/create-each-record.js +163 -0
- package/lib/private/machines/create-manager.js +174 -0
- package/lib/private/machines/create-record.js +126 -0
- package/lib/private/machines/define-physical-model.js +111 -0
- package/lib/private/machines/destroy-manager.js +87 -0
- package/lib/private/machines/destroy-records.js +114 -0
- package/lib/private/machines/drop-physical-model.js +51 -0
- package/lib/private/machines/find-records.js +120 -0
- package/lib/private/machines/get-connection.js +54 -0
- package/lib/private/machines/join.js +259 -0
- package/lib/private/machines/lease-connection.js +58 -0
- package/lib/private/machines/private/build-sqlite-where-clause.js +91 -0
- package/lib/private/machines/private/compile-statement.js +334 -0
- package/lib/private/machines/private/generate-join-sql-query.js +385 -0
- package/lib/private/machines/private/process-each-record.js +106 -0
- package/lib/private/machines/private/process-native-error.js +104 -0
- package/lib/private/machines/private/process-native-record.js +104 -0
- package/lib/private/machines/private/reify-values-to-set.js +83 -0
- package/lib/private/machines/release-connection.js +70 -0
- package/lib/private/machines/rollback-transaction.js +50 -0
- package/lib/private/machines/set-physical-sequence.js +77 -0
- package/lib/private/machines/sum-records.js +75 -0
- package/lib/private/machines/update-records.js +162 -0
- package/lib/private/machines/verify-model-def.js +38 -0
- package/package.json +58 -5
- package/tests/index.js +88 -0
- package/tests/runner.js +99 -0
- package/tests/transaction.test.js +562 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate SQL query for join operations in SQLite
|
|
3
|
+
* This follows SQLite performance best practices for optimal query performance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = function generateJoinSqlQuery(joinCriteria, models, query) {
|
|
7
|
+
const _ = require('@sailshq/lodash')
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// Start building the SELECT clause
|
|
11
|
+
let sqlParts = {
|
|
12
|
+
select: [],
|
|
13
|
+
from: '',
|
|
14
|
+
joins: [],
|
|
15
|
+
where: [],
|
|
16
|
+
orderBy: [],
|
|
17
|
+
limit: '',
|
|
18
|
+
bindings: []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get the primary table info
|
|
22
|
+
const primaryTableName = query.using
|
|
23
|
+
const primaryModel = _.find(models, { tableName: primaryTableName })
|
|
24
|
+
|
|
25
|
+
if (!primaryModel) {
|
|
26
|
+
throw new Error(`Primary model not found for table: ${primaryTableName}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Build SELECT clause - use explicit column names for better performance
|
|
30
|
+
if (query.criteria && query.criteria.select) {
|
|
31
|
+
sqlParts.select = query.criteria.select.map(
|
|
32
|
+
(col) => `${primaryTableName}.${col}`
|
|
33
|
+
)
|
|
34
|
+
} else {
|
|
35
|
+
// Select all columns from primary table with table prefix
|
|
36
|
+
const primaryCols = Object.keys(primaryModel.attributes).map((attr) => {
|
|
37
|
+
const colName = primaryModel.attributes[attr].columnName || attr
|
|
38
|
+
return `${primaryTableName}.${colName}`
|
|
39
|
+
})
|
|
40
|
+
sqlParts.select = primaryCols
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Add joined table columns
|
|
44
|
+
if (joinCriteria && joinCriteria.joins) {
|
|
45
|
+
joinCriteria.joins.forEach((join) => {
|
|
46
|
+
const joinModel = _.find(models, { tableName: join.child })
|
|
47
|
+
if (joinModel) {
|
|
48
|
+
const joinCols = Object.keys(joinModel.attributes).map((attr) => {
|
|
49
|
+
const colName = joinModel.attributes[attr].columnName || attr
|
|
50
|
+
return `${join.child}.${colName} as ${join.child}_${colName}`
|
|
51
|
+
})
|
|
52
|
+
sqlParts.select = sqlParts.select.concat(joinCols)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// FROM clause
|
|
58
|
+
sqlParts.from = primaryTableName
|
|
59
|
+
|
|
60
|
+
// JOIN clauses
|
|
61
|
+
if (joinCriteria && joinCriteria.joins) {
|
|
62
|
+
joinCriteria.joins.forEach((join) => {
|
|
63
|
+
let joinType = 'INNER JOIN' // Default to inner join
|
|
64
|
+
|
|
65
|
+
// Determine join type based on Waterline criteria
|
|
66
|
+
if (join.criteria && join.criteria.where) {
|
|
67
|
+
// This is a simplified check - you might need more sophisticated logic
|
|
68
|
+
joinType = 'LEFT JOIN'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build the JOIN clause with proper foreign key relationships
|
|
72
|
+
const joinClause = `${joinType} ${join.child} ON ${primaryTableName}.${join.parentKey} = ${join.child}.${join.childKey}`
|
|
73
|
+
sqlParts.joins.push(joinClause)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// WHERE clause - handle both primary and join criteria
|
|
78
|
+
const whereConditions = []
|
|
79
|
+
|
|
80
|
+
if (query.criteria && query.criteria.where) {
|
|
81
|
+
const primaryWhere = buildWhereClause(
|
|
82
|
+
query.criteria.where,
|
|
83
|
+
primaryTableName,
|
|
84
|
+
primaryModel
|
|
85
|
+
)
|
|
86
|
+
if (primaryWhere.clause) {
|
|
87
|
+
whereConditions.push(primaryWhere.clause)
|
|
88
|
+
sqlParts.bindings = sqlParts.bindings.concat(primaryWhere.bindings)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Add join-specific where conditions
|
|
93
|
+
if (joinCriteria && joinCriteria.joins) {
|
|
94
|
+
joinCriteria.joins.forEach((join) => {
|
|
95
|
+
if (join.criteria && join.criteria.where) {
|
|
96
|
+
const joinModel = _.find(models, { tableName: join.child })
|
|
97
|
+
if (joinModel) {
|
|
98
|
+
const joinWhere = buildWhereClause(
|
|
99
|
+
join.criteria.where,
|
|
100
|
+
join.child,
|
|
101
|
+
joinModel
|
|
102
|
+
)
|
|
103
|
+
if (joinWhere.clause) {
|
|
104
|
+
whereConditions.push(joinWhere.clause)
|
|
105
|
+
sqlParts.bindings = sqlParts.bindings.concat(joinWhere.bindings)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
sqlParts.where = whereConditions
|
|
113
|
+
|
|
114
|
+
// ORDER BY clause
|
|
115
|
+
if (query.criteria && query.criteria.sort) {
|
|
116
|
+
sqlParts.orderBy = query.criteria.sort.map((sortObj) => {
|
|
117
|
+
const key = Object.keys(sortObj)[0]
|
|
118
|
+
const direction = sortObj[key].toUpperCase()
|
|
119
|
+
return `${primaryTableName}.${key} ${direction}`
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// LIMIT clause
|
|
124
|
+
if (query.criteria && typeof query.criteria.limit === 'number') {
|
|
125
|
+
sqlParts.limit = `LIMIT ${query.criteria.limit}`
|
|
126
|
+
|
|
127
|
+
if (typeof query.criteria.skip === 'number') {
|
|
128
|
+
sqlParts.limit += ` OFFSET ${query.criteria.skip}`
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Assemble the final SQL query
|
|
133
|
+
let sql = `SELECT ${sqlParts.select.join(', ')} FROM \`${sqlParts.from}\``
|
|
134
|
+
|
|
135
|
+
if (sqlParts.joins.length > 0) {
|
|
136
|
+
sql += ' ' + sqlParts.joins.join(' ')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (sqlParts.where.length > 0) {
|
|
140
|
+
sql += ' WHERE ' + sqlParts.where.join(' AND ')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (sqlParts.orderBy.length > 0) {
|
|
144
|
+
sql += ' ORDER BY ' + sqlParts.orderBy.join(', ')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (sqlParts.limit) {
|
|
148
|
+
sql += ' ' + sqlParts.limit
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
sql: sql,
|
|
153
|
+
bindings: sqlParts.bindings
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw new Error(`Error generating join SQL query: ${error.message}`)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build WHERE clause for a given criteria object
|
|
162
|
+
* This handles parameterized queries for SQL injection protection
|
|
163
|
+
*/
|
|
164
|
+
function buildWhereClause(whereObj, tableName, model) {
|
|
165
|
+
const conditions = []
|
|
166
|
+
const bindings = []
|
|
167
|
+
|
|
168
|
+
if (!whereObj || typeof whereObj !== 'object') {
|
|
169
|
+
return { clause: '', bindings: [] }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const [key, value] of Object.entries(whereObj)) {
|
|
173
|
+
if (key === 'and') {
|
|
174
|
+
// Handle AND conditions
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
const andConditions = []
|
|
177
|
+
value.forEach((condition) => {
|
|
178
|
+
const subWhere = buildWhereClause(condition, tableName, model)
|
|
179
|
+
if (subWhere.clause) {
|
|
180
|
+
andConditions.push(subWhere.clause)
|
|
181
|
+
bindings.push(...subWhere.bindings)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
if (andConditions.length > 0) {
|
|
185
|
+
conditions.push(`(${andConditions.join(' AND ')})`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} else if (key === 'or') {
|
|
189
|
+
// Handle OR conditions
|
|
190
|
+
if (Array.isArray(value)) {
|
|
191
|
+
const orConditions = []
|
|
192
|
+
value.forEach((condition) => {
|
|
193
|
+
const subWhere = buildWhereClause(condition, tableName, model)
|
|
194
|
+
if (subWhere.clause) {
|
|
195
|
+
orConditions.push(subWhere.clause)
|
|
196
|
+
bindings.push(...subWhere.bindings)
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
if (orConditions.length > 0) {
|
|
200
|
+
conditions.push(`(${orConditions.join(' OR ')})`)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
// Handle regular field conditions
|
|
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
|
+
}
|
|
217
|
+
|
|
218
|
+
if (typeof value === 'object' && value !== null) {
|
|
219
|
+
// Handle operators like >, <, !=, in, etc.
|
|
220
|
+
for (const [operator, operatorValue] of Object.entries(value)) {
|
|
221
|
+
switch (operator) {
|
|
222
|
+
case '>':
|
|
223
|
+
conditions.push(`${fullColumnName} > ?`)
|
|
224
|
+
bindings.push(operatorValue)
|
|
225
|
+
break
|
|
226
|
+
case '<':
|
|
227
|
+
conditions.push(`${fullColumnName} < ?`)
|
|
228
|
+
bindings.push(operatorValue)
|
|
229
|
+
break
|
|
230
|
+
case '>=':
|
|
231
|
+
conditions.push(`${fullColumnName} >= ?`)
|
|
232
|
+
bindings.push(operatorValue)
|
|
233
|
+
break
|
|
234
|
+
case '<=':
|
|
235
|
+
conditions.push(`${fullColumnName} <= ?`)
|
|
236
|
+
bindings.push(operatorValue)
|
|
237
|
+
break
|
|
238
|
+
case '!=':
|
|
239
|
+
case 'ne':
|
|
240
|
+
conditions.push(`${fullColumnName} != ?`)
|
|
241
|
+
bindings.push(operatorValue)
|
|
242
|
+
break
|
|
243
|
+
case 'in':
|
|
244
|
+
if (Array.isArray(operatorValue) && operatorValue.length > 0) {
|
|
245
|
+
const placeholders = operatorValue.map(() => '?').join(', ')
|
|
246
|
+
conditions.push(`${fullColumnName} IN (${placeholders})`)
|
|
247
|
+
bindings.push(...operatorValue)
|
|
248
|
+
}
|
|
249
|
+
break
|
|
250
|
+
case 'nin':
|
|
251
|
+
if (Array.isArray(operatorValue) && operatorValue.length > 0) {
|
|
252
|
+
const placeholders = operatorValue.map(() => '?').join(', ')
|
|
253
|
+
conditions.push(`${fullColumnName} NOT IN (${placeholders})`)
|
|
254
|
+
bindings.push(...operatorValue)
|
|
255
|
+
}
|
|
256
|
+
break
|
|
257
|
+
case 'like':
|
|
258
|
+
conditions.push(`${fullColumnName} LIKE ?`)
|
|
259
|
+
bindings.push(operatorValue)
|
|
260
|
+
break
|
|
261
|
+
case 'contains':
|
|
262
|
+
conditions.push(`${fullColumnName} LIKE ?`)
|
|
263
|
+
bindings.push(`%${operatorValue}%`)
|
|
264
|
+
break
|
|
265
|
+
case 'startsWith':
|
|
266
|
+
conditions.push(`${fullColumnName} LIKE ?`)
|
|
267
|
+
bindings.push(`${operatorValue}%`)
|
|
268
|
+
break
|
|
269
|
+
case 'endsWith':
|
|
270
|
+
conditions.push(`${fullColumnName} LIKE ?`)
|
|
271
|
+
bindings.push(`%${operatorValue}`)
|
|
272
|
+
break
|
|
273
|
+
default:
|
|
274
|
+
// Default to equality
|
|
275
|
+
conditions.push(`${fullColumnName} = ?`)
|
|
276
|
+
bindings.push(operatorValue)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
// Simple equality
|
|
281
|
+
conditions.push(`${fullColumnName} = ?`)
|
|
282
|
+
bindings.push(value)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
clause: conditions.join(' AND '),
|
|
289
|
+
bindings: bindings
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function generateSqlQuery(joinCriteria, models) {
|
|
294
|
+
const { parentStatement, joins } = joinCriteria
|
|
295
|
+
const tableName = parentStatement.from
|
|
296
|
+
const model = models[tableName]
|
|
297
|
+
|
|
298
|
+
let sql = `SELECT ${tableName}.*`
|
|
299
|
+
const bindings = []
|
|
300
|
+
|
|
301
|
+
// Add select clauses for joined tables
|
|
302
|
+
joins.forEach((join, index) => {
|
|
303
|
+
const joinModel = models[join.childCollectionIdentity]
|
|
304
|
+
const joinAlias = `t${index + 1}`
|
|
305
|
+
Object.keys(joinModel.definition).forEach((attr) => {
|
|
306
|
+
if (joinModel.definition[attr].columnName) {
|
|
307
|
+
sql += `, ${joinAlias}.${joinModel.definition[attr].columnName} AS ${joinAlias}_${attr}`
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
sql += ` FROM \`${tableName}\``
|
|
313
|
+
|
|
314
|
+
// Add join clauses
|
|
315
|
+
joins.forEach((join, index) => {
|
|
316
|
+
const joinType = join.type === 'INNER JOIN' ? 'INNER JOIN' : 'LEFT JOIN'
|
|
317
|
+
const joinAlias = `t${index + 1}`
|
|
318
|
+
sql += ` ${joinType} ${join.childCollectionIdentity} AS ${joinAlias} ON `
|
|
319
|
+
|
|
320
|
+
const joinConditions = []
|
|
321
|
+
Object.keys(join.on).forEach((key) => {
|
|
322
|
+
const parentField = model.definition[key].columnName || key
|
|
323
|
+
const childField =
|
|
324
|
+
models[join.childCollectionIdentity].definition[join.on[key]]
|
|
325
|
+
.columnName || join.on[key]
|
|
326
|
+
joinConditions.push(
|
|
327
|
+
`${tableName}.${parentField} = ${joinAlias}.${childField}`
|
|
328
|
+
)
|
|
329
|
+
})
|
|
330
|
+
sql += joinConditions.join(' AND ')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
// Add where clause
|
|
334
|
+
if (parentStatement.where && Object.keys(parentStatement.where).length > 0) {
|
|
335
|
+
sql += ' WHERE '
|
|
336
|
+
const whereClauses = []
|
|
337
|
+
Object.entries(parentStatement.where).forEach(([key, value]) => {
|
|
338
|
+
if (typeof value === 'object' && value !== null) {
|
|
339
|
+
Object.entries(value).forEach(([operator, operand]) => {
|
|
340
|
+
switch (operator) {
|
|
341
|
+
case 'in':
|
|
342
|
+
whereClauses.push(
|
|
343
|
+
`${tableName}.${key} IN (${operand.map(() => '?').join(', ')})`
|
|
344
|
+
)
|
|
345
|
+
bindings.push(...operand)
|
|
346
|
+
break
|
|
347
|
+
case 'like':
|
|
348
|
+
whereClauses.push(`${tableName}.${key} LIKE ?`)
|
|
349
|
+
bindings.push(operand)
|
|
350
|
+
break
|
|
351
|
+
// Add more operators as needed
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
} else {
|
|
355
|
+
whereClauses.push(`${tableName}.${key} = ?`)
|
|
356
|
+
bindings.push(value)
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
sql += whereClauses.join(' AND ')
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Add order by clause
|
|
363
|
+
if (parentStatement.sort && parentStatement.sort.length > 0) {
|
|
364
|
+
sql +=
|
|
365
|
+
' ORDER BY ' +
|
|
366
|
+
parentStatement.sort
|
|
367
|
+
.map((sortClause) => {
|
|
368
|
+
const direction = sortClause.dir === 'desc' ? 'DESC' : 'ASC'
|
|
369
|
+
return `${tableName}.${sortClause.attrName} ${direction}`
|
|
370
|
+
})
|
|
371
|
+
.join(', ')
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Add limit and skip
|
|
375
|
+
if (parentStatement.limit) {
|
|
376
|
+
sql += ' LIMIT ?'
|
|
377
|
+
bindings.push(parentStatement.limit)
|
|
378
|
+
}
|
|
379
|
+
if (parentStatement.skip) {
|
|
380
|
+
sql += ' OFFSET ?'
|
|
381
|
+
bindings.push(parentStatement.skip)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return { sql, bindings }
|
|
385
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process Each Record
|
|
3
|
+
*
|
|
4
|
+
* Process an array of records, transforming them from their raw SQLite format
|
|
5
|
+
* to the format expected by Waterline. This follows performance best practices.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const _ = require('@sailshq/lodash')
|
|
9
|
+
const { eachRecordDeep } = require('waterline-utils')
|
|
10
|
+
|
|
11
|
+
module.exports = function processEachRecord(options) {
|
|
12
|
+
// Validate options
|
|
13
|
+
if (!options || typeof options !== 'object') {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'Invalid options argument. Options must contain: records, identity, and orm.'
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { records, identity, orm } = options
|
|
20
|
+
|
|
21
|
+
if (!Array.isArray(records)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'Invalid option used in options argument. Missing or invalid records.'
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
if (typeof identity !== 'string') {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'Invalid option used in options argument. Missing or invalid identity.'
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
if (typeof orm !== 'object') {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'Invalid option used in options argument. Missing or invalid orm.'
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Key the collections by identity instead of column name
|
|
38
|
+
const collections = Object.fromEntries(
|
|
39
|
+
Object.entries(orm.collections).map(([key, val]) => [val.identity, val])
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// Update the orm object with the keyed collections
|
|
43
|
+
orm.collections = collections
|
|
44
|
+
|
|
45
|
+
// Process each record
|
|
46
|
+
eachRecordDeep(
|
|
47
|
+
records,
|
|
48
|
+
(record, WLModel) => {
|
|
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) => {
|
|
56
|
+
const columnName = attrDef.columnName || attrName
|
|
57
|
+
|
|
58
|
+
if (columnName in record) {
|
|
59
|
+
switch (attrDef.type) {
|
|
60
|
+
case 'boolean':
|
|
61
|
+
// SQLite stores booleans as integers, so we need to convert them
|
|
62
|
+
if (typeof record[columnName] !== 'boolean') {
|
|
63
|
+
record[columnName] = record[columnName] === 1
|
|
64
|
+
}
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
case 'json':
|
|
68
|
+
// SQLite stores JSON as text, so we need to parse it
|
|
69
|
+
if (record[columnName] !== null) {
|
|
70
|
+
try {
|
|
71
|
+
record[columnName] = JSON.parse(record[columnName])
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.warn(
|
|
74
|
+
`Failed to parse JSON for attribute ${attrName}:`,
|
|
75
|
+
e
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
case 'number':
|
|
82
|
+
// Ensure numbers are actually numbers
|
|
83
|
+
record[columnName] = Number(record[columnName])
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
case 'date':
|
|
87
|
+
case 'datetime':
|
|
88
|
+
// SQLite doesn't have a native date type, so we need to parse it
|
|
89
|
+
if (
|
|
90
|
+
record[columnName] &&
|
|
91
|
+
typeof record[columnName] === 'string'
|
|
92
|
+
) {
|
|
93
|
+
record[columnName] = new Date(record[columnName])
|
|
94
|
+
}
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
// Add more type conversions as needed
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
},
|
|
102
|
+
true,
|
|
103
|
+
identity,
|
|
104
|
+
orm
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const flaverr = require('flaverr')
|
|
2
|
+
|
|
3
|
+
module.exports = function processNativeError(err) {
|
|
4
|
+
if (err.footprint !== undefined) {
|
|
5
|
+
return new Error(
|
|
6
|
+
`Consistency violation: Raw error from SQLite arrived with a pre-existing \`footprint\` property! Should never happen... but maybe this error didn't actually come from SQLite..? Here's the error:\n\n\`\`\`\n${err.stack}\n\`\`\`\n`
|
|
7
|
+
)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// better-sqlite3 uses string-based error codes
|
|
11
|
+
switch (err.code) {
|
|
12
|
+
case 'SQLITE_CONSTRAINT':
|
|
13
|
+
case 'SQLITE_CONSTRAINT_UNIQUE':
|
|
14
|
+
// Check if it's a UNIQUE constraint violation
|
|
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
|
|
20
|
+
const match = err.message.match(/UNIQUE constraint failed: \w+\.(\w+)/)
|
|
21
|
+
const keys = match && match[1] ? [match[1]] : []
|
|
22
|
+
|
|
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
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
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
|
+
)
|
|
50
|
+
|
|
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
|
+
)
|
|
76
|
+
|
|
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
|
+
)
|
|
89
|
+
|
|
90
|
+
default:
|
|
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
|
+
)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const assert = require('assert')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* processNativeRecord()
|
|
5
|
+
*
|
|
6
|
+
* Modify a native record coming back from the SQLite database so that it matches
|
|
7
|
+
* the expectations of the adapter spec (i.e. still a physical record, but
|
|
8
|
+
* minus any database-specific eccentricities).
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} nativeRecord
|
|
11
|
+
* @param {Object} WLModel
|
|
12
|
+
* @param {Object?} meta [`meta` query key from the s3q]
|
|
13
|
+
*/
|
|
14
|
+
module.exports = function processNativeRecord(nativeRecord, WLModel, meta) {
|
|
15
|
+
assert(nativeRecord !== undefined, '1st argument is required')
|
|
16
|
+
assert(
|
|
17
|
+
typeof nativeRecord === 'object' &&
|
|
18
|
+
nativeRecord !== null &&
|
|
19
|
+
!Array.isArray(nativeRecord),
|
|
20
|
+
'1st argument must be a dictionary'
|
|
21
|
+
)
|
|
22
|
+
assert(WLModel !== undefined, '2nd argument is required')
|
|
23
|
+
assert(
|
|
24
|
+
typeof WLModel === 'object' && WLModel !== null && !Array.isArray(WLModel),
|
|
25
|
+
'2nd argument must be a WLModel, and it has to have a `definition` property for this utility to work.'
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// Check out each known attribute...
|
|
29
|
+
Object.entries(WLModel.attributes).forEach(([attrName, attrDef]) => {
|
|
30
|
+
const phRecordKey = attrDef.columnName
|
|
31
|
+
|
|
32
|
+
// Handle JSON type
|
|
33
|
+
if (
|
|
34
|
+
attrDef.type === 'json' &&
|
|
35
|
+
typeof nativeRecord[phRecordKey] === 'string'
|
|
36
|
+
) {
|
|
37
|
+
try {
|
|
38
|
+
nativeRecord[phRecordKey] = JSON.parse(nativeRecord[phRecordKey])
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// If parsing fails, leave the value as-is
|
|
41
|
+
console.warn(
|
|
42
|
+
`Failed to parse JSON for attribute ${attrName}: ${e.message}`
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handle Date type
|
|
48
|
+
if (
|
|
49
|
+
attrDef.type === 'ref' &&
|
|
50
|
+
typeof nativeRecord[phRecordKey] === 'string'
|
|
51
|
+
) {
|
|
52
|
+
const timestamp = Date.parse(nativeRecord[phRecordKey])
|
|
53
|
+
if (!isNaN(timestamp)) {
|
|
54
|
+
nativeRecord[phRecordKey] = new Date(timestamp)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle Number type - SQLite returns numbers as strings with decimals
|
|
59
|
+
if (
|
|
60
|
+
attrDef.type === 'number' &&
|
|
61
|
+
typeof nativeRecord[phRecordKey] === 'string'
|
|
62
|
+
) {
|
|
63
|
+
const numericValue = parseFloat(nativeRecord[phRecordKey])
|
|
64
|
+
if (!isNaN(numericValue)) {
|
|
65
|
+
nativeRecord[phRecordKey] = numericValue
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle Boolean type
|
|
70
|
+
if (attrDef.type === 'boolean') {
|
|
71
|
+
// SQLite stores booleans as integers (0 = false, 1 = true)
|
|
72
|
+
// But they come back as strings, so we need to parse them first
|
|
73
|
+
const rawValue = nativeRecord[phRecordKey]
|
|
74
|
+
if (rawValue !== undefined && rawValue !== null) {
|
|
75
|
+
const numericValue =
|
|
76
|
+
typeof rawValue === 'string' ? parseFloat(rawValue) : rawValue
|
|
77
|
+
nativeRecord[phRecordKey] = numericValue === 0 ? false : true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isForeignKey = !!attrDef.model
|
|
82
|
+
// Sanity checks:
|
|
83
|
+
if (isForeignKey) {
|
|
84
|
+
assert(
|
|
85
|
+
attrDef.foreignKey,
|
|
86
|
+
'attribute has a `model` property, but wl-schema did not give it `foreignKey: true`!'
|
|
87
|
+
)
|
|
88
|
+
} else {
|
|
89
|
+
assert(
|
|
90
|
+
!attrDef.foreignKey,
|
|
91
|
+
'wl-schema gave this attribute `foreignKey: true`, but it has no `model` property!'
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isForeignKey) return
|
|
96
|
+
if (nativeRecord[phRecordKey] === undefined) return // This is weird, but WL core deals with warning about it.
|
|
97
|
+
if (nativeRecord[phRecordKey] === null) return
|
|
98
|
+
|
|
99
|
+
// For SQLite, we don't need to do any special processing for foreign keys
|
|
100
|
+
// as they are typically just stored as integers or strings.
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return nativeRecord
|
|
104
|
+
}
|