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.
Files changed (48) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/workflows/prettier.yml +16 -0
  3. package/.github/workflows/test.yml +16 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.prettierrc.js +5 -0
  6. package/CHANGELOG.md +161 -0
  7. package/LICENSE +21 -0
  8. package/README.md +247 -0
  9. package/lib/index.js +1104 -0
  10. package/lib/private/build-std-adapter-method.js +69 -0
  11. package/lib/private/constants/connection.input.js +15 -0
  12. package/lib/private/constants/dry-orm.input.js +23 -0
  13. package/lib/private/constants/meta.input.js +14 -0
  14. package/lib/private/constants/not-unique.exit.js +16 -0
  15. package/lib/private/constants/query.input.js +15 -0
  16. package/lib/private/constants/table-name.input.js +12 -0
  17. package/lib/private/machines/avg-records.js +74 -0
  18. package/lib/private/machines/begin-transaction.js +51 -0
  19. package/lib/private/machines/commit-transaction.js +50 -0
  20. package/lib/private/machines/count-records.js +78 -0
  21. package/lib/private/machines/create-each-record.js +163 -0
  22. package/lib/private/machines/create-manager.js +174 -0
  23. package/lib/private/machines/create-record.js +126 -0
  24. package/lib/private/machines/define-physical-model.js +111 -0
  25. package/lib/private/machines/destroy-manager.js +87 -0
  26. package/lib/private/machines/destroy-records.js +114 -0
  27. package/lib/private/machines/drop-physical-model.js +51 -0
  28. package/lib/private/machines/find-records.js +120 -0
  29. package/lib/private/machines/get-connection.js +54 -0
  30. package/lib/private/machines/join.js +259 -0
  31. package/lib/private/machines/lease-connection.js +58 -0
  32. package/lib/private/machines/private/build-sqlite-where-clause.js +91 -0
  33. package/lib/private/machines/private/compile-statement.js +334 -0
  34. package/lib/private/machines/private/generate-join-sql-query.js +385 -0
  35. package/lib/private/machines/private/process-each-record.js +106 -0
  36. package/lib/private/machines/private/process-native-error.js +104 -0
  37. package/lib/private/machines/private/process-native-record.js +104 -0
  38. package/lib/private/machines/private/reify-values-to-set.js +83 -0
  39. package/lib/private/machines/release-connection.js +70 -0
  40. package/lib/private/machines/rollback-transaction.js +50 -0
  41. package/lib/private/machines/set-physical-sequence.js +77 -0
  42. package/lib/private/machines/sum-records.js +75 -0
  43. package/lib/private/machines/update-records.js +162 -0
  44. package/lib/private/machines/verify-model-def.js +38 -0
  45. package/package.json +58 -5
  46. package/tests/index.js +88 -0
  47. package/tests/runner.js +99 -0
  48. 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
+ }