sails-sqlite 0.2.0 → 0.2.2
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 +12 -1
- package/lib/private/machines/compile-statement.js +62 -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/compile-statement.js +48 -0
- package/lib/private/machines/private/process-each-record.js +12 -2
- package/lib/private/machines/send-native-query.js +100 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -19,6 +19,10 @@ const DRY_MACHINES = {
|
|
|
19
19
|
dropPhysicalModel: require('./private/machines/drop-physical-model'),
|
|
20
20
|
setPhysicalSequence: require('./private/machines/set-physical-sequence'),
|
|
21
21
|
join: require('./private/machines/join'),
|
|
22
|
+
sendNativeQuery: require('./private/machines/send-native-query'),
|
|
23
|
+
compileStatement: require('./private/machines/compile-statement'),
|
|
24
|
+
parseNativeQueryResult: require('./private/machines/parse-native-query-result'),
|
|
25
|
+
parseNativeQueryError: require('./private/machines/parse-native-query-error'),
|
|
22
26
|
beginTransaction: require('./private/machines/begin-transaction'),
|
|
23
27
|
commitTransaction: require('./private/machines/commit-transaction'),
|
|
24
28
|
rollbackTransaction: require('./private/machines/rollback-transaction'),
|
|
@@ -88,7 +92,7 @@ module.exports = {
|
|
|
88
92
|
// Default configuration for connections
|
|
89
93
|
defaults: {
|
|
90
94
|
schema: false,
|
|
91
|
-
url: 'db/
|
|
95
|
+
url: 'db/local.db',
|
|
92
96
|
pragmas: {
|
|
93
97
|
journal_mode: 'WAL'
|
|
94
98
|
}
|
|
@@ -233,6 +237,13 @@ module.exports = {
|
|
|
233
237
|
destroyManager: WET_MACHINES.destroyManager,
|
|
234
238
|
getConnection: WET_MACHINES.getConnection,
|
|
235
239
|
releaseConnection: WET_MACHINES.releaseConnection,
|
|
240
|
+
sendNativeQuery: WET_MACHINES.sendNativeQuery,
|
|
241
|
+
compileStatement: WET_MACHINES.compileStatement,
|
|
242
|
+
parseNativeQueryResult: WET_MACHINES.parseNativeQueryResult,
|
|
243
|
+
parseNativeQueryError: WET_MACHINES.parseNativeQueryError,
|
|
244
|
+
beginTransaction: WET_MACHINES.beginTransaction,
|
|
245
|
+
commitTransaction: WET_MACHINES.commitTransaction,
|
|
246
|
+
rollbackTransaction: WET_MACHINES.rollbackTransaction,
|
|
236
247
|
Database
|
|
237
248
|
}
|
|
238
249
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const compileStatementUtil = require('./private/compile-statement')
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
friendlyName: 'Compile statement',
|
|
5
|
+
|
|
6
|
+
description: 'Compile a Waterline statement to a native query for SQLite.',
|
|
7
|
+
|
|
8
|
+
inputs: {
|
|
9
|
+
statement: {
|
|
10
|
+
description:
|
|
11
|
+
'A Waterline statement (stage 4 query) to compile into a native query.',
|
|
12
|
+
extendedDescription:
|
|
13
|
+
'See documentation for info on Waterline statements.',
|
|
14
|
+
example: '===',
|
|
15
|
+
required: true
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
meta: {
|
|
19
|
+
friendlyName: 'Meta (custom)',
|
|
20
|
+
description: 'Additional stuff to pass to the driver.',
|
|
21
|
+
extendedDescription:
|
|
22
|
+
'This is reserved for custom driver-specific extensions.',
|
|
23
|
+
example: '==='
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
exits: {
|
|
28
|
+
success: {
|
|
29
|
+
description:
|
|
30
|
+
'The provided Waterline statement was compiled successfully.',
|
|
31
|
+
outputVariableName: 'report',
|
|
32
|
+
outputDescription:
|
|
33
|
+
'The `nativeQuery` property is the compiled native query for the database. The `valuesToEscape` property is an array of strings, numbers, or special literals (true, false, or null) to escape and include in the query, in order. The `meta` property is reserved for custom driver-specific extensions.',
|
|
34
|
+
outputExample: '==='
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
malformed: {
|
|
38
|
+
description: 'The provided Waterline statement could not be compiled.',
|
|
39
|
+
outputVariableName: 'report',
|
|
40
|
+
outputDescription:
|
|
41
|
+
'The `error` property is a JavaScript Error instance with more details about what went wrong. The `meta` property is reserved for custom driver-specific extensions.',
|
|
42
|
+
outputExample: '==='
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
fn: function compileStatement(inputs, exits) {
|
|
47
|
+
try {
|
|
48
|
+
const compiled = compileStatementUtil(inputs.statement)
|
|
49
|
+
|
|
50
|
+
return exits.success({
|
|
51
|
+
nativeQuery: compiled.sql,
|
|
52
|
+
valuesToEscape: compiled.bindings || [],
|
|
53
|
+
meta: inputs.meta
|
|
54
|
+
})
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return exits.malformed({
|
|
57
|
+
error: err,
|
|
58
|
+
meta: inputs.meta
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Parse native query error',
|
|
3
|
+
|
|
4
|
+
description:
|
|
5
|
+
'Attempt to identify and parse a raw error from sending a native query and normalize it into a standard error footprint.',
|
|
6
|
+
|
|
7
|
+
inputs: {
|
|
8
|
+
nativeQueryError: {
|
|
9
|
+
description:
|
|
10
|
+
'The error sent back from the database as a result of a native query.',
|
|
11
|
+
extendedDescription:
|
|
12
|
+
'Specifically, this is the Error returned from sendNativeQuery().',
|
|
13
|
+
example: '===',
|
|
14
|
+
required: true
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
meta: {
|
|
18
|
+
friendlyName: 'Meta (custom)',
|
|
19
|
+
description: 'Additional stuff to pass to the driver.',
|
|
20
|
+
extendedDescription:
|
|
21
|
+
'This is reserved for custom driver-specific extensions.',
|
|
22
|
+
example: '==='
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
exits: {
|
|
27
|
+
success: {
|
|
28
|
+
description: 'The error was parsed successfully.',
|
|
29
|
+
outputVariableName: 'report',
|
|
30
|
+
outputDescription:
|
|
31
|
+
'The `footprint` property is the normalized "footprint" dictionary representing the provided error.',
|
|
32
|
+
outputExample: '==='
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
malformed: {
|
|
36
|
+
description:
|
|
37
|
+
'The provided error cannot be parsed (perhaps it was corrupted or otherwise incomplete).',
|
|
38
|
+
outputVariableName: 'report',
|
|
39
|
+
outputDescription:
|
|
40
|
+
'The `error` property is a JavaScript Error instance with more details about what went wrong. The `meta` property is reserved for custom driver-specific extensions.',
|
|
41
|
+
outputExample: '==='
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
notUnique: {
|
|
45
|
+
description: 'A uniqueness constraint was violated.',
|
|
46
|
+
outputVariableName: 'report',
|
|
47
|
+
outputDescription:
|
|
48
|
+
'The `footprint` property contains details about the uniqueness error.',
|
|
49
|
+
outputExample: '==='
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
notFound: {
|
|
53
|
+
description: 'No record(s) found matching the specified criteria.',
|
|
54
|
+
outputVariableName: 'report',
|
|
55
|
+
outputDescription:
|
|
56
|
+
'The `footprint` property contains details about the not found error.',
|
|
57
|
+
outputExample: '==='
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
fn: function parseNativeQueryError(inputs, exits) {
|
|
62
|
+
try {
|
|
63
|
+
const err = inputs.nativeQueryError
|
|
64
|
+
const footprint = {
|
|
65
|
+
identity: 'unknown',
|
|
66
|
+
error: err,
|
|
67
|
+
raw: err
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (err.code === 'SQLITE_CONSTRAINT') {
|
|
71
|
+
if (err.message && err.message.includes('UNIQUE')) {
|
|
72
|
+
return exits.notUnique({
|
|
73
|
+
footprint: footprint,
|
|
74
|
+
meta: inputs.meta
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return exits.success({
|
|
80
|
+
footprint: footprint,
|
|
81
|
+
meta: inputs.meta
|
|
82
|
+
})
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return exits.malformed({
|
|
85
|
+
error: err,
|
|
86
|
+
meta: inputs.meta
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Parse native query result',
|
|
3
|
+
|
|
4
|
+
description: 'Parse a raw result from a native query and normalize it.',
|
|
5
|
+
|
|
6
|
+
inputs: {
|
|
7
|
+
nativeQueryResult: {
|
|
8
|
+
description:
|
|
9
|
+
'The result data sent back from the the database as a result of a native query.',
|
|
10
|
+
extendedDescription:
|
|
11
|
+
'Specifically, this is the `result` returned by sendNativeQuery().',
|
|
12
|
+
example: '===',
|
|
13
|
+
required: true
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
meta: {
|
|
17
|
+
friendlyName: 'Meta (custom)',
|
|
18
|
+
description: 'Additional stuff to pass to the driver.',
|
|
19
|
+
extendedDescription:
|
|
20
|
+
'This is reserved for custom driver-specific extensions.',
|
|
21
|
+
example: '==='
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
exits: {
|
|
26
|
+
success: {
|
|
27
|
+
description: 'The result was parsed successfully.',
|
|
28
|
+
outputVariableName: 'report',
|
|
29
|
+
outputDescription:
|
|
30
|
+
'The `result` property is the parsed result. The `meta` property is reserved for custom driver-specific extensions.',
|
|
31
|
+
outputExample: '==='
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
couldNotParse: {
|
|
35
|
+
description:
|
|
36
|
+
'The provided result could not be parsed (it might be corrupted or in an unexpected format).',
|
|
37
|
+
outputVariableName: 'report',
|
|
38
|
+
outputDescription:
|
|
39
|
+
'The `error` property is a JavaScript Error instance with more details about what went wrong. The `meta` property is reserved for custom driver-specific extensions.',
|
|
40
|
+
outputExample: '==='
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
fn: function parseNativeQueryResult(inputs, exits) {
|
|
45
|
+
try {
|
|
46
|
+
const result = inputs.nativeQueryResult
|
|
47
|
+
|
|
48
|
+
return exits.success({
|
|
49
|
+
result: result.rows || [],
|
|
50
|
+
meta: inputs.meta
|
|
51
|
+
})
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return exits.couldNotParse({
|
|
54
|
+
error: err,
|
|
55
|
+
meta: inputs.meta
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -43,12 +43,26 @@ module.exports = function compileStatement(statement) {
|
|
|
43
43
|
) {
|
|
44
44
|
const orderClauses = globalOrderBy.map((orderItem) => {
|
|
45
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
|
+
}
|
|
46
53
|
return `\`${orderItem}\` ASC`
|
|
47
54
|
}
|
|
48
55
|
if (typeof orderItem === 'object') {
|
|
49
56
|
const key = Object.keys(orderItem)[0]
|
|
50
57
|
const direction =
|
|
51
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
|
+
}
|
|
52
66
|
return `\`${key}\` ${direction}`
|
|
53
67
|
}
|
|
54
68
|
return orderItem
|
|
@@ -61,6 +75,18 @@ module.exports = function compileStatement(statement) {
|
|
|
61
75
|
|
|
62
76
|
// Handle regular SELECT statements
|
|
63
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
|
+
|
|
64
90
|
// SELECT clause
|
|
65
91
|
if (Array.isArray(statement.select) && statement.select.length > 0) {
|
|
66
92
|
const selectColumns = statement.select.map((col) => {
|
|
@@ -168,12 +194,34 @@ module.exports = function compileStatement(statement) {
|
|
|
168
194
|
) {
|
|
169
195
|
const orderClauses = statement.orderBy.map((orderItem) => {
|
|
170
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
|
+
}
|
|
171
208
|
return `\`${orderItem}\` ASC`
|
|
172
209
|
}
|
|
173
210
|
if (typeof orderItem === 'object') {
|
|
174
211
|
const key = Object.keys(orderItem)[0]
|
|
175
212
|
const direction =
|
|
176
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
|
+
}
|
|
177
225
|
return `\`${key}\` ${direction}`
|
|
178
226
|
}
|
|
179
227
|
return orderItem
|
|
@@ -55,6 +55,11 @@ module.exports = function processEachRecord(options) {
|
|
|
55
55
|
_.each(WLModel.definition, (attrDef, attrName) => {
|
|
56
56
|
const columnName = attrDef.columnName || attrName
|
|
57
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
|
+
|
|
58
63
|
if (columnName in record) {
|
|
59
64
|
switch (attrDef.type) {
|
|
60
65
|
case 'boolean':
|
|
@@ -66,7 +71,10 @@ module.exports = function processEachRecord(options) {
|
|
|
66
71
|
|
|
67
72
|
case 'json':
|
|
68
73
|
// SQLite stores JSON as text, so we need to parse it
|
|
69
|
-
if (
|
|
74
|
+
if (
|
|
75
|
+
record[columnName] !== null &&
|
|
76
|
+
typeof record[columnName] === 'string'
|
|
77
|
+
) {
|
|
70
78
|
try {
|
|
71
79
|
record[columnName] = JSON.parse(record[columnName])
|
|
72
80
|
} catch (e) {
|
|
@@ -80,7 +88,9 @@ module.exports = function processEachRecord(options) {
|
|
|
80
88
|
|
|
81
89
|
case 'number':
|
|
82
90
|
// Ensure numbers are actually numbers
|
|
83
|
-
record[columnName]
|
|
91
|
+
if (typeof record[columnName] !== 'number') {
|
|
92
|
+
record[columnName] = Number(record[columnName])
|
|
93
|
+
}
|
|
84
94
|
break
|
|
85
95
|
|
|
86
96
|
case 'date':
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Send native query',
|
|
3
|
+
|
|
4
|
+
description: 'Send a native query to the SQLite database.',
|
|
5
|
+
|
|
6
|
+
inputs: {
|
|
7
|
+
connection: {
|
|
8
|
+
friendlyName: 'Connection',
|
|
9
|
+
description: 'An active database connection.',
|
|
10
|
+
extendedDescription:
|
|
11
|
+
'The provided database connection instance must still be active. Only database connection instances created by the `getConnection()` machine in this driver are supported.',
|
|
12
|
+
example: '===',
|
|
13
|
+
required: true
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
nativeQuery: {
|
|
17
|
+
description: 'A native query for the database.',
|
|
18
|
+
extendedDescription:
|
|
19
|
+
'If `valuesToEscape` is provided, this supports template syntax like `?` for SQLite parameter binding.',
|
|
20
|
+
example: 'SELECT * FROM pets WHERE species=? AND nickname=?',
|
|
21
|
+
required: true
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
valuesToEscape: {
|
|
25
|
+
description:
|
|
26
|
+
'An optional list of strings, numbers, or special literals (true, false, or null) to escape and include in the native query, in order.',
|
|
27
|
+
extendedDescription:
|
|
28
|
+
'The first value in the list will be used to replace the first `?`, the second value to replace the second `?`, and so on.',
|
|
29
|
+
example: '===',
|
|
30
|
+
defaultsTo: []
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
meta: {
|
|
34
|
+
friendlyName: 'Meta (custom)',
|
|
35
|
+
description: 'Additional stuff to pass to the driver.',
|
|
36
|
+
extendedDescription:
|
|
37
|
+
'This is reserved for custom driver-specific extensions.',
|
|
38
|
+
example: '==='
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
exits: {
|
|
43
|
+
success: {
|
|
44
|
+
description: 'The native query was executed successfully.',
|
|
45
|
+
outputVariableName: 'report',
|
|
46
|
+
outputDescription:
|
|
47
|
+
'The `result` property is the result data the database sent back. The `meta` property is reserved for custom driver-specific extensions.',
|
|
48
|
+
outputExample: '==='
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
queryFailed: {
|
|
52
|
+
description:
|
|
53
|
+
'The database returned an error when attempting to execute the native query.',
|
|
54
|
+
outputVariableName: 'report',
|
|
55
|
+
outputDescription:
|
|
56
|
+
'The `error` property is a JavaScript Error instance with more details about what went wrong. The `meta` property is reserved for custom driver-specific extensions.',
|
|
57
|
+
outputExample: '==='
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
badConnection: {
|
|
61
|
+
friendlyName: 'Bad connection',
|
|
62
|
+
description: 'The provided connection is not valid or no longer active.',
|
|
63
|
+
outputVariableName: 'report',
|
|
64
|
+
outputDescription:
|
|
65
|
+
'The `meta` property is reserved for custom driver-specific extensions.',
|
|
66
|
+
outputExample: '==='
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
fn: function sendNativeQuery(inputs, exits) {
|
|
71
|
+
if (!inputs.connection || typeof inputs.connection.prepare !== 'function') {
|
|
72
|
+
return exits.badConnection()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const sql = inputs.nativeQuery
|
|
76
|
+
const bindings = inputs.valuesToEscape || []
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const stmt = inputs.connection.prepare(sql)
|
|
80
|
+
const isSelect =
|
|
81
|
+
sql.trim().toUpperCase().startsWith('SELECT') ||
|
|
82
|
+
sql.trim().toUpperCase().startsWith('PRAGMA')
|
|
83
|
+
const result = isSelect ? stmt.all(...bindings) : stmt.run(...bindings)
|
|
84
|
+
|
|
85
|
+
return exits.success({
|
|
86
|
+
result: {
|
|
87
|
+
rows: isSelect ? result : [],
|
|
88
|
+
changes: result.changes || 0,
|
|
89
|
+
lastInsertRowid: result.lastInsertRowid
|
|
90
|
+
},
|
|
91
|
+
meta: inputs.meta
|
|
92
|
+
})
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return exits.queryFailed({
|
|
95
|
+
error: err,
|
|
96
|
+
meta: inputs.meta
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|