sails-sqlite 0.0.0 → 0.1.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 +928 -0
- package/lib/private/build-std-adapter-method.js +65 -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/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 +102 -0
- package/lib/private/machines/destroy-manager.js +87 -0
- package/lib/private/machines/destroy-records.js +101 -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 +93 -0
- package/lib/private/machines/private/build-sqlite-where-clause.js +89 -0
- package/lib/private/machines/private/generate-join-sql-query.js +377 -0
- package/lib/private/machines/private/process-each-record.js +99 -0
- package/lib/private/machines/private/process-native-error.js +59 -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/set-physical-sequence.js +77 -0
- package/lib/private/machines/sum-records.js +75 -0
- package/lib/private/machines/update-records.js +145 -0
- package/lib/private/machines/verify-model-def.js +38 -0
- package/package.json +53 -5
- package/tests/adapter.test.js +534 -0
- package/tests/datatypes.test.js +293 -0
- package/tests/index.js +88 -0
- package/tests/sequence.test.js +153 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const processEachRecord = require('./private/process-each-record')
|
|
2
|
+
const generateJoinSqlQuery = require('./private/generate-join-sql-query')
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
friendlyName: 'Join',
|
|
6
|
+
|
|
7
|
+
description: 'Perform a join operation in SQLite using better-sqlite3.',
|
|
8
|
+
|
|
9
|
+
inputs: {
|
|
10
|
+
datastore: {
|
|
11
|
+
description: 'The datastore to use for the query.',
|
|
12
|
+
required: true,
|
|
13
|
+
example: '==='
|
|
14
|
+
},
|
|
15
|
+
models: {
|
|
16
|
+
description:
|
|
17
|
+
'An object containing all of the model definitions that have been registered.',
|
|
18
|
+
required: true,
|
|
19
|
+
example: '==='
|
|
20
|
+
},
|
|
21
|
+
query: {
|
|
22
|
+
description: 'A normalized Waterline Stage Three Query.',
|
|
23
|
+
required: true,
|
|
24
|
+
example: '==='
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
exits: {
|
|
29
|
+
success: {
|
|
30
|
+
description: 'The query was run successfully.',
|
|
31
|
+
outputType: 'ref'
|
|
32
|
+
},
|
|
33
|
+
error: {
|
|
34
|
+
description: 'An error occurred while performing the query.'
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
fn: async function (inputs, exits) {
|
|
39
|
+
const { datastore, models, query } = inputs
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const { joins } = require('waterline-utils')
|
|
43
|
+
|
|
44
|
+
// Convert join criteria using waterline-utils
|
|
45
|
+
const joinCriteria = joins.convertJoinCriteria({
|
|
46
|
+
query,
|
|
47
|
+
getPk: (tableName) => {
|
|
48
|
+
// Find the model by tableName
|
|
49
|
+
let targetModel = null
|
|
50
|
+
for (const modelIdentity in models) {
|
|
51
|
+
if (models[modelIdentity].tableName === tableName) {
|
|
52
|
+
targetModel = models[modelIdentity]
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!targetModel) {
|
|
58
|
+
throw new Error(`No model found with tableName: ${tableName}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pkAttrName = targetModel.primaryKey
|
|
62
|
+
const pkDef = targetModel.attributes[pkAttrName]
|
|
63
|
+
return pkDef.columnName || pkAttrName
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Generate SQL query using our helper
|
|
68
|
+
const { sql, bindings } = generateJoinSqlQuery(
|
|
69
|
+
joinCriteria,
|
|
70
|
+
models,
|
|
71
|
+
query
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
// Execute the query using the database connection from the datastore
|
|
75
|
+
const db = datastore.manager
|
|
76
|
+
const stmt = db.prepare(sql)
|
|
77
|
+
const results = stmt.all(...bindings)
|
|
78
|
+
|
|
79
|
+
// Process results through the join utility
|
|
80
|
+
const processedResults = joins.processJoinResults({
|
|
81
|
+
query,
|
|
82
|
+
records: results,
|
|
83
|
+
orm: {
|
|
84
|
+
collections: models
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return exits.success(processedResults)
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return exits.error(error)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildSqliteWhereClause()
|
|
3
|
+
*
|
|
4
|
+
* Build a SQLite WHERE clause from the specified S3Q `where` clause.
|
|
5
|
+
* > Note: The provided `where` clause is NOT mutated.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} whereClause [`where` clause from the criteria of a S3Q]
|
|
8
|
+
* @param {Object} WLModel
|
|
9
|
+
* @param {Object?} meta [`meta` query key from the s3q]
|
|
10
|
+
*
|
|
11
|
+
* @returns {String} [SQLite WHERE clause]
|
|
12
|
+
*/
|
|
13
|
+
module.exports = function buildSqliteWhereClause(whereClause, WLModel, meta) {
|
|
14
|
+
// Handle empty `where` clause.
|
|
15
|
+
if (Object.keys(whereClause).length === 0) {
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Recursively build and return a transformed `where` clause for use with SQLite.
|
|
20
|
+
function recurse(branch, isRoot = true) {
|
|
21
|
+
const clauses = []
|
|
22
|
+
for (const [key, value] of Object.entries(branch)) {
|
|
23
|
+
if (key === 'and' || key === 'or') {
|
|
24
|
+
const subClauses = value.map((subBranch) => recurse(subBranch, false))
|
|
25
|
+
clauses.push(`(${subClauses.join(` ${key.toUpperCase()} `)})`)
|
|
26
|
+
} else {
|
|
27
|
+
clauses.push(buildConstraint(key, value, WLModel, meta))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return isRoot ? clauses.join(' AND ') : clauses.join(' AND ')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return recurse(whereClause)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildConstraint(columnName, constraint, WLModel, meta) {
|
|
37
|
+
if (typeof constraint !== 'object' || constraint === null) {
|
|
38
|
+
return `${columnName} = ${sqliteEscape(constraint)}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const modifierKind = Object.keys(constraint)[0]
|
|
42
|
+
const modifier = constraint[modifierKind]
|
|
43
|
+
|
|
44
|
+
switch (modifierKind) {
|
|
45
|
+
case '<':
|
|
46
|
+
return `${columnName} < ${sqliteEscape(modifier)}`
|
|
47
|
+
case '<=':
|
|
48
|
+
return `${columnName} <= ${sqliteEscape(modifier)}`
|
|
49
|
+
case '>':
|
|
50
|
+
return `${columnName} > ${sqliteEscape(modifier)}`
|
|
51
|
+
case '>=':
|
|
52
|
+
return `${columnName} >= ${sqliteEscape(modifier)}`
|
|
53
|
+
case '!=':
|
|
54
|
+
return `${columnName} != ${sqliteEscape(modifier)}`
|
|
55
|
+
case 'nin':
|
|
56
|
+
return `${columnName} NOT IN (${modifier.map(sqliteEscape).join(', ')})`
|
|
57
|
+
case 'in':
|
|
58
|
+
return `${columnName} IN (${modifier.map(sqliteEscape).join(', ')})`
|
|
59
|
+
case 'like':
|
|
60
|
+
let likePattern = modifier
|
|
61
|
+
.replace(/^%/, '.*')
|
|
62
|
+
.replace(/([^\\])%/g, '$1.*')
|
|
63
|
+
.replace(/\\%/g, '%')
|
|
64
|
+
likePattern = `^${likePattern}$`
|
|
65
|
+
let clause = `${columnName} REGEXP '${likePattern}'`
|
|
66
|
+
if (meta && meta.makeLikeModifierCaseInsensitive === true) {
|
|
67
|
+
clause = `LOWER(${columnName}) REGEXP '${likePattern.toLowerCase()}'`
|
|
68
|
+
}
|
|
69
|
+
return clause
|
|
70
|
+
default:
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Consistency violation: \`where\` clause modifier \`${modifierKind}\` is not valid! This should never happen-- a stage 3 query should have already been normalized in Waterline core.`
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sqliteEscape(value) {
|
|
78
|
+
if (typeof value === 'string') {
|
|
79
|
+
return `'${value.replace(/'/g, "''")}'`
|
|
80
|
+
}
|
|
81
|
+
if (typeof value === 'boolean') {
|
|
82
|
+
// Match the decimal format that SQLite stores (1.0, 0.0)
|
|
83
|
+
return value ? '1.0' : '0.0'
|
|
84
|
+
}
|
|
85
|
+
if (value === null) {
|
|
86
|
+
return 'NULL'
|
|
87
|
+
}
|
|
88
|
+
return value
|
|
89
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
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
|
+
const columnName = model.attributes[key]
|
|
206
|
+
? model.attributes[key].columnName || key
|
|
207
|
+
: key
|
|
208
|
+
const fullColumnName = `${tableName}.${columnName}`
|
|
209
|
+
|
|
210
|
+
if (typeof value === 'object' && value !== null) {
|
|
211
|
+
// Handle operators like >, <, !=, in, etc.
|
|
212
|
+
for (const [operator, operatorValue] of Object.entries(value)) {
|
|
213
|
+
switch (operator) {
|
|
214
|
+
case '>':
|
|
215
|
+
conditions.push(`${fullColumnName} > ?`)
|
|
216
|
+
bindings.push(operatorValue)
|
|
217
|
+
break
|
|
218
|
+
case '<':
|
|
219
|
+
conditions.push(`${fullColumnName} < ?`)
|
|
220
|
+
bindings.push(operatorValue)
|
|
221
|
+
break
|
|
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
|
+
case 'ne':
|
|
232
|
+
conditions.push(`${fullColumnName} != ?`)
|
|
233
|
+
bindings.push(operatorValue)
|
|
234
|
+
break
|
|
235
|
+
case 'in':
|
|
236
|
+
if (Array.isArray(operatorValue) && operatorValue.length > 0) {
|
|
237
|
+
const placeholders = operatorValue.map(() => '?').join(', ')
|
|
238
|
+
conditions.push(`${fullColumnName} IN (${placeholders})`)
|
|
239
|
+
bindings.push(...operatorValue)
|
|
240
|
+
}
|
|
241
|
+
break
|
|
242
|
+
case 'nin':
|
|
243
|
+
if (Array.isArray(operatorValue) && operatorValue.length > 0) {
|
|
244
|
+
const placeholders = operatorValue.map(() => '?').join(', ')
|
|
245
|
+
conditions.push(`${fullColumnName} NOT IN (${placeholders})`)
|
|
246
|
+
bindings.push(...operatorValue)
|
|
247
|
+
}
|
|
248
|
+
break
|
|
249
|
+
case 'like':
|
|
250
|
+
conditions.push(`${fullColumnName} LIKE ?`)
|
|
251
|
+
bindings.push(operatorValue)
|
|
252
|
+
break
|
|
253
|
+
case 'contains':
|
|
254
|
+
conditions.push(`${fullColumnName} LIKE ?`)
|
|
255
|
+
bindings.push(`%${operatorValue}%`)
|
|
256
|
+
break
|
|
257
|
+
case 'startsWith':
|
|
258
|
+
conditions.push(`${fullColumnName} LIKE ?`)
|
|
259
|
+
bindings.push(`${operatorValue}%`)
|
|
260
|
+
break
|
|
261
|
+
case 'endsWith':
|
|
262
|
+
conditions.push(`${fullColumnName} LIKE ?`)
|
|
263
|
+
bindings.push(`%${operatorValue}`)
|
|
264
|
+
break
|
|
265
|
+
default:
|
|
266
|
+
// Default to equality
|
|
267
|
+
conditions.push(`${fullColumnName} = ?`)
|
|
268
|
+
bindings.push(operatorValue)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
// Simple equality
|
|
273
|
+
conditions.push(`${fullColumnName} = ?`)
|
|
274
|
+
bindings.push(value)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
clause: conditions.join(' AND '),
|
|
281
|
+
bindings: bindings
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function generateSqlQuery(joinCriteria, models) {
|
|
286
|
+
const { parentStatement, joins } = joinCriteria
|
|
287
|
+
const tableName = parentStatement.from
|
|
288
|
+
const model = models[tableName]
|
|
289
|
+
|
|
290
|
+
let sql = `SELECT ${tableName}.*`
|
|
291
|
+
const bindings = []
|
|
292
|
+
|
|
293
|
+
// Add select clauses for joined tables
|
|
294
|
+
joins.forEach((join, index) => {
|
|
295
|
+
const joinModel = models[join.childCollectionIdentity]
|
|
296
|
+
const joinAlias = `t${index + 1}`
|
|
297
|
+
Object.keys(joinModel.definition).forEach((attr) => {
|
|
298
|
+
if (joinModel.definition[attr].columnName) {
|
|
299
|
+
sql += `, ${joinAlias}.${joinModel.definition[attr].columnName} AS ${joinAlias}_${attr}`
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
sql += ` FROM ${tableName}`
|
|
305
|
+
|
|
306
|
+
// Add join clauses
|
|
307
|
+
joins.forEach((join, index) => {
|
|
308
|
+
const joinType = join.type === 'INNER JOIN' ? 'INNER JOIN' : 'LEFT JOIN'
|
|
309
|
+
const joinAlias = `t${index + 1}`
|
|
310
|
+
sql += ` ${joinType} ${join.childCollectionIdentity} AS ${joinAlias} ON `
|
|
311
|
+
|
|
312
|
+
const joinConditions = []
|
|
313
|
+
Object.keys(join.on).forEach((key) => {
|
|
314
|
+
const parentField = model.definition[key].columnName || key
|
|
315
|
+
const childField =
|
|
316
|
+
models[join.childCollectionIdentity].definition[join.on[key]]
|
|
317
|
+
.columnName || join.on[key]
|
|
318
|
+
joinConditions.push(
|
|
319
|
+
`${tableName}.${parentField} = ${joinAlias}.${childField}`
|
|
320
|
+
)
|
|
321
|
+
})
|
|
322
|
+
sql += joinConditions.join(' AND ')
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// Add where clause
|
|
326
|
+
if (parentStatement.where && Object.keys(parentStatement.where).length > 0) {
|
|
327
|
+
sql += ' WHERE '
|
|
328
|
+
const whereClauses = []
|
|
329
|
+
Object.entries(parentStatement.where).forEach(([key, value]) => {
|
|
330
|
+
if (typeof value === 'object' && value !== null) {
|
|
331
|
+
Object.entries(value).forEach(([operator, operand]) => {
|
|
332
|
+
switch (operator) {
|
|
333
|
+
case 'in':
|
|
334
|
+
whereClauses.push(
|
|
335
|
+
`${tableName}.${key} IN (${operand.map(() => '?').join(', ')})`
|
|
336
|
+
)
|
|
337
|
+
bindings.push(...operand)
|
|
338
|
+
break
|
|
339
|
+
case 'like':
|
|
340
|
+
whereClauses.push(`${tableName}.${key} LIKE ?`)
|
|
341
|
+
bindings.push(operand)
|
|
342
|
+
break
|
|
343
|
+
// Add more operators as needed
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
} else {
|
|
347
|
+
whereClauses.push(`${tableName}.${key} = ?`)
|
|
348
|
+
bindings.push(value)
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
sql += whereClauses.join(' AND ')
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Add order by clause
|
|
355
|
+
if (parentStatement.sort && parentStatement.sort.length > 0) {
|
|
356
|
+
sql +=
|
|
357
|
+
' ORDER BY ' +
|
|
358
|
+
parentStatement.sort
|
|
359
|
+
.map((sortClause) => {
|
|
360
|
+
const direction = sortClause.dir === 'desc' ? 'DESC' : 'ASC'
|
|
361
|
+
return `${tableName}.${sortClause.attrName} ${direction}`
|
|
362
|
+
})
|
|
363
|
+
.join(', ')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Add limit and skip
|
|
367
|
+
if (parentStatement.limit) {
|
|
368
|
+
sql += ' LIMIT ?'
|
|
369
|
+
bindings.push(parentStatement.limit)
|
|
370
|
+
}
|
|
371
|
+
if (parentStatement.skip) {
|
|
372
|
+
sql += ' OFFSET ?'
|
|
373
|
+
bindings.push(parentStatement.skip)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return { sql, bindings }
|
|
377
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { eachRecordDeep } = require('waterline-utils')
|
|
9
|
+
|
|
10
|
+
module.exports = function processEachRecord(options) {
|
|
11
|
+
// Validate options
|
|
12
|
+
if (!options || typeof options !== 'object') {
|
|
13
|
+
throw new Error(
|
|
14
|
+
'Invalid options argument. Options must contain: records, identity, and orm.'
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { records, identity, orm } = options
|
|
19
|
+
|
|
20
|
+
if (!Array.isArray(records)) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'Invalid option used in options argument. Missing or invalid records.'
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
if (typeof identity !== 'string') {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Invalid option used in options argument. Missing or invalid identity.'
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
if (typeof orm !== 'object') {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'Invalid option used in options argument. Missing or invalid orm.'
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Key the collections by identity instead of column name
|
|
37
|
+
const collections = Object.fromEntries(
|
|
38
|
+
Object.entries(orm.collections).map(([key, val]) => [val.identity, val])
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// Update the orm object with the keyed collections
|
|
42
|
+
orm.collections = collections
|
|
43
|
+
|
|
44
|
+
// Process each record
|
|
45
|
+
eachRecordDeep(
|
|
46
|
+
records,
|
|
47
|
+
(record, WLModel) => {
|
|
48
|
+
for (const [attrName, attrDef] of Object.entries(WLModel.definition)) {
|
|
49
|
+
const columnName = attrDef.columnName || attrName
|
|
50
|
+
|
|
51
|
+
if (columnName in record) {
|
|
52
|
+
switch (attrDef.type) {
|
|
53
|
+
case 'boolean':
|
|
54
|
+
// SQLite stores booleans as integers, so we need to convert them
|
|
55
|
+
if (typeof record[columnName] !== 'boolean') {
|
|
56
|
+
record[columnName] = record[columnName] === 1
|
|
57
|
+
}
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
case 'json':
|
|
61
|
+
// SQLite stores JSON as text, so we need to parse it
|
|
62
|
+
if (record[columnName] !== null) {
|
|
63
|
+
try {
|
|
64
|
+
record[columnName] = JSON.parse(record[columnName])
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.warn(
|
|
67
|
+
`Failed to parse JSON for attribute ${attrName}:`,
|
|
68
|
+
e
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
case 'number':
|
|
75
|
+
// Ensure numbers are actually numbers
|
|
76
|
+
record[columnName] = Number(record[columnName])
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
case 'date':
|
|
80
|
+
case 'datetime':
|
|
81
|
+
// SQLite doesn't have a native date type, so we need to parse it
|
|
82
|
+
if (
|
|
83
|
+
record[columnName] &&
|
|
84
|
+
typeof record[columnName] === 'string'
|
|
85
|
+
) {
|
|
86
|
+
record[columnName] = new Date(record[columnName])
|
|
87
|
+
}
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
// Add more type conversions as needed
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
true,
|
|
96
|
+
identity,
|
|
97
|
+
orm
|
|
98
|
+
)
|
|
99
|
+
}
|