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,83 @@
|
|
|
1
|
+
const assert = require('assert')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* reifyValuesToSet()
|
|
5
|
+
*
|
|
6
|
+
* Prepare a dictionary of values to be used in a SQLite database operation.
|
|
7
|
+
* > The provided `valuesToSet` will be mutated in-place.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} valuesToSet
|
|
10
|
+
* @param {Object} WLModel
|
|
11
|
+
* @param {Object?} meta [`meta` query key from the s3q]
|
|
12
|
+
*/
|
|
13
|
+
module.exports = function reifyValuesToSet(valuesToSet, WLModel, meta) {
|
|
14
|
+
assert(valuesToSet !== undefined, '1st argument is required')
|
|
15
|
+
assert(
|
|
16
|
+
typeof valuesToSet === 'object' &&
|
|
17
|
+
valuesToSet !== null &&
|
|
18
|
+
!Array.isArray(valuesToSet) &&
|
|
19
|
+
typeof valuesToSet !== 'function',
|
|
20
|
+
'1st argument must be a dictionary'
|
|
21
|
+
)
|
|
22
|
+
assert(WLModel !== undefined, '2nd argument is required')
|
|
23
|
+
assert(
|
|
24
|
+
typeof WLModel === 'object' &&
|
|
25
|
+
WLModel !== null &&
|
|
26
|
+
!Array.isArray(WLModel) &&
|
|
27
|
+
typeof WLModel !== 'function',
|
|
28
|
+
'2nd argument must be a WLModel, and it has to have a `definition` property for this utility to work.'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// Handle primary key safely
|
|
32
|
+
if (
|
|
33
|
+
WLModel.primaryKey &&
|
|
34
|
+
WLModel.attributes &&
|
|
35
|
+
WLModel.attributes[WLModel.primaryKey]
|
|
36
|
+
) {
|
|
37
|
+
const primaryKeyAttrName = WLModel.primaryKey
|
|
38
|
+
const primaryKeyAttrDef = WLModel.attributes[WLModel.primaryKey]
|
|
39
|
+
const primaryKeyColumnName =
|
|
40
|
+
primaryKeyAttrDef.columnName || primaryKeyAttrName
|
|
41
|
+
|
|
42
|
+
if (valuesToSet[primaryKeyColumnName] === null) {
|
|
43
|
+
delete valuesToSet[primaryKeyColumnName]
|
|
44
|
+
} else if (valuesToSet[primaryKeyColumnName] !== undefined) {
|
|
45
|
+
// Ensure primary key is a number or string
|
|
46
|
+
if (
|
|
47
|
+
typeof valuesToSet[primaryKeyColumnName] !== 'number' &&
|
|
48
|
+
typeof valuesToSet[primaryKeyColumnName] !== 'string'
|
|
49
|
+
) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Invalid primary key value provided for \`${primaryKeyAttrName}\`. Must be a number or string.`
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Handle other attributes safely
|
|
58
|
+
if (WLModel.attributes && typeof WLModel.attributes === 'object') {
|
|
59
|
+
Object.entries(WLModel.attributes).forEach(([attrName, attrDef]) => {
|
|
60
|
+
if (!attrDef || typeof attrDef !== 'object') return
|
|
61
|
+
|
|
62
|
+
const columnName = attrDef.columnName || attrName
|
|
63
|
+
if (valuesToSet[columnName] === undefined) return
|
|
64
|
+
|
|
65
|
+
// Handle JSON type
|
|
66
|
+
if (attrDef.type === 'json' && valuesToSet[columnName] !== null) {
|
|
67
|
+
valuesToSet[columnName] = JSON.stringify(valuesToSet[columnName])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle date type
|
|
71
|
+
if (attrDef.type === 'ref' && valuesToSet[columnName] instanceof Date) {
|
|
72
|
+
valuesToSet[columnName] = valuesToSet[columnName].toISOString()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Handle boolean type
|
|
76
|
+
if (attrDef.type === 'boolean') {
|
|
77
|
+
valuesToSet[columnName] = valuesToSet[columnName] ? 1 : 0
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return valuesToSet
|
|
83
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Release connection',
|
|
3
|
+
|
|
4
|
+
description: 'Release an active SQLite database connection.',
|
|
5
|
+
|
|
6
|
+
extendedDescription:
|
|
7
|
+
"For SQLite, this is typically a no-op as there is no connection pooling. However, it's included for consistency with other database adapters.",
|
|
8
|
+
|
|
9
|
+
sync: true,
|
|
10
|
+
|
|
11
|
+
inputs: {
|
|
12
|
+
connection: {
|
|
13
|
+
description: 'An active SQLite database connection.',
|
|
14
|
+
extendedDescription:
|
|
15
|
+
'The provided database connection instance must still be active. Only database connection instances created by the `getConnection()` function in this driver are supported.',
|
|
16
|
+
example: '===',
|
|
17
|
+
required: true
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
meta: {
|
|
21
|
+
friendlyName: 'Meta (custom)',
|
|
22
|
+
description: 'Additional stuff to pass to the driver.',
|
|
23
|
+
extendedDescription:
|
|
24
|
+
'This is reserved for custom driver-specific extensions. Please refer to the documentation for better-sqlite3 for more specific information.',
|
|
25
|
+
example: '==='
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
exits: {
|
|
30
|
+
success: {
|
|
31
|
+
description: 'The connection was released (no-op for SQLite).',
|
|
32
|
+
extendedDescription:
|
|
33
|
+
"For SQLite, this is typically a no-op, but it's included for consistency.",
|
|
34
|
+
outputFriendlyName: 'Report',
|
|
35
|
+
outputDescription:
|
|
36
|
+
'The `meta` property is reserved for custom driver-specific extensions.',
|
|
37
|
+
outputExample: '==='
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
badConnection: {
|
|
41
|
+
description: 'The provided connection is not a valid SQLite connection.',
|
|
42
|
+
extendedDescription:
|
|
43
|
+
'This might occur if the connection was already closed or if an invalid object was passed as the connection.',
|
|
44
|
+
outputFriendlyName: 'Report',
|
|
45
|
+
outputDescription:
|
|
46
|
+
'The `meta` property is reserved for custom driver-specific extensions.',
|
|
47
|
+
outputExample: '==='
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
fn: ({ connection, meta }, exits) => {
|
|
52
|
+
// Check if the connection is a valid SQLite database instance
|
|
53
|
+
if (
|
|
54
|
+
typeof connection !== 'object' ||
|
|
55
|
+
connection === null ||
|
|
56
|
+
typeof connection.close !== 'function'
|
|
57
|
+
) {
|
|
58
|
+
return exits.badConnection({
|
|
59
|
+
meta
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// For SQLite, releasing a connection is typically a no-op
|
|
64
|
+
// We don't actually close the connection here because SQLite connections
|
|
65
|
+
// are meant to be long-lived and are automatically closed when the database is closed
|
|
66
|
+
return exits.success({
|
|
67
|
+
meta
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module dependencies
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Rollback Transaction
|
|
7
|
+
*
|
|
8
|
+
* Rollback the current database transaction on the provided connection.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
friendlyName: 'Rollback transaction',
|
|
13
|
+
|
|
14
|
+
description:
|
|
15
|
+
'Rollback the current database transaction on the provided connection.',
|
|
16
|
+
|
|
17
|
+
moreInfoUrl:
|
|
18
|
+
'https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#transactionfunction---function',
|
|
19
|
+
|
|
20
|
+
inputs: {
|
|
21
|
+
connection: {
|
|
22
|
+
description:
|
|
23
|
+
'An active database connection that was acquired from a manager.',
|
|
24
|
+
example: '===',
|
|
25
|
+
required: true
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
meta: {
|
|
29
|
+
description: 'Additional options for this query.',
|
|
30
|
+
example: '==='
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
fn: function rollbackTransaction(inputs, exits) {
|
|
35
|
+
const db = inputs.connection
|
|
36
|
+
const meta = inputs.meta || {}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (!db.inTransaction) {
|
|
40
|
+
return exits.error(new Error('No active transaction to rollback.'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
db.prepare('ROLLBACK TRANSACTION').run()
|
|
44
|
+
|
|
45
|
+
return exits.success()
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return exits.error(err)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Set physical sequence',
|
|
3
|
+
|
|
4
|
+
description: 'Reset an auto-incrementing sequence to the specified value.',
|
|
5
|
+
|
|
6
|
+
sideEffects: 'idempotent',
|
|
7
|
+
|
|
8
|
+
inputs: {
|
|
9
|
+
connection: require('../constants/connection.input'),
|
|
10
|
+
sequenceName: { example: 'users', required: true },
|
|
11
|
+
sequenceValue: { example: 1, required: true },
|
|
12
|
+
meta: require('../constants/meta.input')
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
exits: {
|
|
16
|
+
success: {
|
|
17
|
+
description: 'The sequence was successfully reset.'
|
|
18
|
+
},
|
|
19
|
+
notFound: {
|
|
20
|
+
description: 'Could not find a sequence with the specified name.'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
fn: function (inputs, exits) {
|
|
25
|
+
const db = inputs.connection
|
|
26
|
+
const sequenceName = inputs.sequenceName
|
|
27
|
+
const newSequenceValue = inputs.sequenceValue
|
|
28
|
+
|
|
29
|
+
// Parse the sequence name to get the actual table name
|
|
30
|
+
// PostgreSQL-style sequences are often named like 'user_id_seq', 'users_id_seq', etc.
|
|
31
|
+
// The table name should be the first string before the first underscore
|
|
32
|
+
let tableName = sequenceName
|
|
33
|
+
|
|
34
|
+
// Handle PostgreSQL-style sequence names
|
|
35
|
+
if (sequenceName.includes('_')) {
|
|
36
|
+
// Extract the table name as the first part before the first underscore
|
|
37
|
+
tableName = sequenceName.split('_')[0]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// First, check if the table exists
|
|
42
|
+
const tableExists = db
|
|
43
|
+
.prepare(
|
|
44
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
|
|
45
|
+
)
|
|
46
|
+
.get(tableName)
|
|
47
|
+
|
|
48
|
+
if (!tableExists) {
|
|
49
|
+
return exits.notFound(new Error(`Table '${tableName}' not found.`))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If the table exists, update the sequence
|
|
53
|
+
const updateStmt = db.prepare(
|
|
54
|
+
'UPDATE sqlite_sequence SET seq = ? WHERE name = ?'
|
|
55
|
+
)
|
|
56
|
+
const updateResult = updateStmt.run(newSequenceValue - 1, tableName)
|
|
57
|
+
|
|
58
|
+
if (updateResult.changes === 0) {
|
|
59
|
+
// If no rows were updated, it means the table doesn't have an autoincrement column
|
|
60
|
+
// We'll insert a new row in this case
|
|
61
|
+
const insertStmt = db.prepare(
|
|
62
|
+
'INSERT INTO sqlite_sequence (name, seq) VALUES (?, ?)'
|
|
63
|
+
)
|
|
64
|
+
insertStmt.run(tableName, newSequenceValue - 1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return exits.success()
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// Handle the case where sqlite_sequence doesn't exist
|
|
70
|
+
if (error.message.includes('no such table: sqlite_sequence')) {
|
|
71
|
+
// This is not an error condition - it just means no tables with AUTOINCREMENT have been created yet
|
|
72
|
+
return exits.success()
|
|
73
|
+
}
|
|
74
|
+
return exits.error(error)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const buildSqliteWhereClause = require('./private/build-sqlite-where-clause')
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
friendlyName: 'Sum (records)',
|
|
5
|
+
|
|
6
|
+
description:
|
|
7
|
+
'Return the cumulative sum (∑) of a particular property over matching records.',
|
|
8
|
+
|
|
9
|
+
inputs: {
|
|
10
|
+
query: require('../constants/query.input'),
|
|
11
|
+
connection: require('../constants/connection.input'),
|
|
12
|
+
dryOrm: require('../constants/dry-orm.input')
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
exits: {
|
|
16
|
+
success: {
|
|
17
|
+
outputFriendlyName: 'Total (sum)',
|
|
18
|
+
outputDescription:
|
|
19
|
+
'The sum of the given property across all matching records.',
|
|
20
|
+
outputExample: 999.99
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
fn: function sum(inputs, exits) {
|
|
25
|
+
const s3q = inputs.query
|
|
26
|
+
|
|
27
|
+
const tableName = s3q.using
|
|
28
|
+
const numericFieldName = s3q.numericAttrName
|
|
29
|
+
|
|
30
|
+
// Grab the model definition
|
|
31
|
+
// Find model by tableName since models is an object, not an array
|
|
32
|
+
let WLModel = null
|
|
33
|
+
for (const modelIdentity in inputs.dryOrm.models) {
|
|
34
|
+
if (inputs.dryOrm.models[modelIdentity].tableName === tableName) {
|
|
35
|
+
WLModel = inputs.dryOrm.models[modelIdentity]
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!WLModel) {
|
|
40
|
+
return exits.error(
|
|
41
|
+
new Error(
|
|
42
|
+
`No model with that tableName (\`${tableName}\`) has been registered with this adapter. Were any unexpected modifications made to the stage 3 query? Could the adapter's internal state have been corrupted? (This error is usually due to a bug in this adapter's implementation.)`
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Build a SQLite WHERE clause from the `where` clause.
|
|
48
|
+
let whereClause
|
|
49
|
+
try {
|
|
50
|
+
whereClause = buildSqliteWhereClause(
|
|
51
|
+
s3q.criteria.where,
|
|
52
|
+
WLModel,
|
|
53
|
+
s3q.meta
|
|
54
|
+
)
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return exits.error(e)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const db = inputs.connection
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
let sumQuery = `SELECT COALESCE(SUM(\`${numericFieldName}\`), 0) as total FROM \`${tableName}\``
|
|
63
|
+
if (whereClause) {
|
|
64
|
+
sumQuery += ` WHERE ${whereClause}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const stmt = db.prepare(sumQuery)
|
|
68
|
+
const result = stmt.get()
|
|
69
|
+
|
|
70
|
+
return exits.success(result.total)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return exits.error(err)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const util = require('util')
|
|
2
|
+
const processNativeRecord = require('./private/process-native-record')
|
|
3
|
+
const processNativeError = require('./private/process-native-error')
|
|
4
|
+
const reifyValuesToSet = require('./private/reify-values-to-set')
|
|
5
|
+
const buildSqliteWhereClause = require('./private/build-sqlite-where-clause')
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
friendlyName: 'Update (records)',
|
|
9
|
+
|
|
10
|
+
description:
|
|
11
|
+
'Update record(s) in the SQLite database based on a query criteria.',
|
|
12
|
+
|
|
13
|
+
inputs: {
|
|
14
|
+
query: require('../constants/query.input'),
|
|
15
|
+
connection: require('../constants/connection.input'),
|
|
16
|
+
dryOrm: require('../constants/dry-orm.input')
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
exits: {
|
|
20
|
+
success: {
|
|
21
|
+
outputFriendlyName: 'Records (maybe)',
|
|
22
|
+
outputDescription:
|
|
23
|
+
'Either `null` OR (if `fetch:true`) an array of physical records that were updated.',
|
|
24
|
+
outputExample: '==='
|
|
25
|
+
},
|
|
26
|
+
notUnique: require('../constants/not-unique.exit')
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
fn: async function (inputs, exits) {
|
|
30
|
+
const s3q = inputs.query
|
|
31
|
+
if (s3q.meta && s3q.meta.logSqliteS3Qs) {
|
|
32
|
+
console.log(
|
|
33
|
+
'* * * * * *\nADAPTER (UPDATE RECORDS):',
|
|
34
|
+
util.inspect(s3q, { depth: 5 }),
|
|
35
|
+
'\n'
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tableName = s3q.using
|
|
40
|
+
// Find model by tableName since models is an object, not an array
|
|
41
|
+
let WLModel = null
|
|
42
|
+
for (const modelIdentity in inputs.dryOrm.models) {
|
|
43
|
+
if (inputs.dryOrm.models[modelIdentity].tableName === tableName) {
|
|
44
|
+
WLModel = inputs.dryOrm.models[modelIdentity]
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!WLModel) {
|
|
50
|
+
return exits.error(
|
|
51
|
+
new Error(
|
|
52
|
+
`No model with that tableName (\`${tableName}\`) has been registered with this adapter. Were any unexpected modifications made to the stage 3 query? Could the adapter's internal state have been corrupted? (This error is usually due to a bug in this adapter's implementation.)`
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const pkColumnName = WLModel.attributes[WLModel.primaryKey].columnName
|
|
58
|
+
|
|
59
|
+
const isFetchEnabled = !!(s3q.meta && s3q.meta.fetch)
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
reifyValuesToSet(s3q.valuesToSet, WLModel, s3q.meta)
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return exits.error(e)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sqliteWhere = buildSqliteWhereClause(
|
|
68
|
+
s3q.criteria.where,
|
|
69
|
+
WLModel,
|
|
70
|
+
s3q.meta
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const db = inputs.connection
|
|
74
|
+
|
|
75
|
+
// Check if we're already in a transaction
|
|
76
|
+
const wasInTransaction = db.inTransaction
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Start a transaction only if we're not already in one
|
|
80
|
+
if (!wasInTransaction) {
|
|
81
|
+
db.exec('BEGIN TRANSACTION')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let affectedIds = []
|
|
85
|
+
|
|
86
|
+
if (isFetchEnabled) {
|
|
87
|
+
// Get the IDs of records which match this criteria
|
|
88
|
+
const selectSql = sqliteWhere
|
|
89
|
+
? `SELECT \`${pkColumnName}\` FROM \`${tableName}\` WHERE ${sqliteWhere}`
|
|
90
|
+
: `SELECT \`${pkColumnName}\` FROM \`${tableName}\``
|
|
91
|
+
const selectStmt = db.prepare(selectSql)
|
|
92
|
+
affectedIds = selectStmt.all().map((row) => row[pkColumnName])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Prepare the UPDATE statement
|
|
96
|
+
const setClauses = Object.entries(s3q.valuesToSet)
|
|
97
|
+
.map(([column, value]) => `\`${column}\` = ?`)
|
|
98
|
+
.join(', ')
|
|
99
|
+
const updateSql = sqliteWhere
|
|
100
|
+
? `UPDATE \`${tableName}\` SET ${setClauses} WHERE ${sqliteWhere}`
|
|
101
|
+
: `UPDATE \`${tableName}\` SET ${setClauses}`
|
|
102
|
+
const updateStmt = db.prepare(updateSql)
|
|
103
|
+
|
|
104
|
+
// Execute the UPDATE
|
|
105
|
+
const updateInfo = updateStmt.run(...Object.values(s3q.valuesToSet))
|
|
106
|
+
|
|
107
|
+
// Handle case where pk value was changed
|
|
108
|
+
if (
|
|
109
|
+
s3q.valuesToSet[pkColumnName] !== undefined &&
|
|
110
|
+
affectedIds.length === 1
|
|
111
|
+
) {
|
|
112
|
+
const oldPkValue = affectedIds[0]
|
|
113
|
+
const newPkValue = s3q.valuesToSet[pkColumnName]
|
|
114
|
+
affectedIds = [newPkValue]
|
|
115
|
+
} else if (
|
|
116
|
+
s3q.valuesToSet[pkColumnName] !== undefined &&
|
|
117
|
+
affectedIds.length > 1
|
|
118
|
+
) {
|
|
119
|
+
if (!wasInTransaction) {
|
|
120
|
+
db.exec('ROLLBACK')
|
|
121
|
+
}
|
|
122
|
+
return exits.error(
|
|
123
|
+
new Error(
|
|
124
|
+
'Consistency violation: Updated multiple records to have the same primary key value. (PK values should be unique!)'
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// If fetch is not enabled, we're done
|
|
130
|
+
if (!isFetchEnabled) {
|
|
131
|
+
if (!wasInTransaction) {
|
|
132
|
+
db.exec('COMMIT')
|
|
133
|
+
}
|
|
134
|
+
return exits.success()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fetch the updated records
|
|
138
|
+
const fetchSql = `SELECT * FROM \`${tableName}\` WHERE \`${pkColumnName}\` IN (${affectedIds.map(() => '?').join(', ')})`
|
|
139
|
+
const fetchStmt = db.prepare(fetchSql)
|
|
140
|
+
const phRecords = fetchStmt.all(affectedIds)
|
|
141
|
+
|
|
142
|
+
// Process records
|
|
143
|
+
phRecords.forEach((phRecord) => {
|
|
144
|
+
processNativeRecord(phRecord, WLModel, s3q.meta)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (!wasInTransaction) {
|
|
148
|
+
db.exec('COMMIT')
|
|
149
|
+
}
|
|
150
|
+
return exits.success(phRecords)
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (!wasInTransaction) {
|
|
153
|
+
db.exec('ROLLBACK')
|
|
154
|
+
}
|
|
155
|
+
err = processNativeError(err)
|
|
156
|
+
if (err.footprint && err.footprint.identity === 'notUnique') {
|
|
157
|
+
return exits.notUnique(err)
|
|
158
|
+
}
|
|
159
|
+
return exits.error(err)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Verify model def',
|
|
3
|
+
|
|
4
|
+
description:
|
|
5
|
+
'Verify that the specified model definition is compatible with this adapter.',
|
|
6
|
+
|
|
7
|
+
extendedDescription:
|
|
8
|
+
'This assumes that the provided model def has already undergone adapter-agnostic normalization, and is considered generally valid.',
|
|
9
|
+
|
|
10
|
+
sideEffects: 'cacheable',
|
|
11
|
+
|
|
12
|
+
sync: true,
|
|
13
|
+
|
|
14
|
+
inputs: {
|
|
15
|
+
modelDef: {
|
|
16
|
+
description: 'A Waterline model definition.',
|
|
17
|
+
extendedDescription:
|
|
18
|
+
'This model definition should already be fully-formed (i.e. it should have undergone generic normalization/validation already).',
|
|
19
|
+
moreInfoUrl:
|
|
20
|
+
'http://sailsjs.com/documentation/concepts/models-and-orm/models',
|
|
21
|
+
example: '===', // {}
|
|
22
|
+
readOnly: true,
|
|
23
|
+
required: true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
exits: {
|
|
28
|
+
invalid: {
|
|
29
|
+
description: 'The provided model definition was invalid.',
|
|
30
|
+
outputFriendlyName: 'Error',
|
|
31
|
+
outputExample: '===' // e.g. new Error('Primary key attribute should have `columnName: \'_id\'`.')
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
fn: function (inputs, exits) {
|
|
36
|
+
return exits.success()
|
|
37
|
+
}
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sails-sqlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "SQLite adapter for Sails/Waterline",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "lib",
|
|
6
|
+
"directories": {
|
|
7
|
+
"lib": "./lib"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
|
-
"test": "npm run lint && npm run custom-tests"
|
|
10
|
+
"test": "npm run lint && npm run custom-tests && npm run adapter-tests",
|
|
11
|
+
"custom-tests": "node tests/",
|
|
12
|
+
"adapter-tests": "rm -rf .tmp && node tests/runner.js",
|
|
13
|
+
"lint": "prettier --check .",
|
|
14
|
+
"lint:fix": "prettier --write .",
|
|
15
|
+
"prepare": "husky"
|
|
8
16
|
},
|
|
9
17
|
"repository": {
|
|
10
18
|
"type": "git",
|
|
11
|
-
"url": "github.com/sailscastshq/sails-sqlite"
|
|
19
|
+
"url": "git://github.com/sailscastshq/sails-sqlite.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": "https://github.com/sailscastshq/sails-sqlite/issues",
|
|
22
|
+
"waterlineAdapter": {
|
|
23
|
+
"waterlineVersion": "~0.13.0",
|
|
24
|
+
"interfaces": [
|
|
25
|
+
"semantic",
|
|
26
|
+
"queryable",
|
|
27
|
+
"migratable",
|
|
28
|
+
"associations",
|
|
29
|
+
"sql"
|
|
30
|
+
],
|
|
31
|
+
"features": [
|
|
32
|
+
"crossAdapter",
|
|
33
|
+
"unique",
|
|
34
|
+
"autoIncrement",
|
|
35
|
+
"schemas"
|
|
36
|
+
]
|
|
12
37
|
},
|
|
13
38
|
"keywords": [
|
|
14
39
|
"sqlite",
|
|
@@ -21,5 +46,33 @@
|
|
|
21
46
|
"database-adapter"
|
|
22
47
|
],
|
|
23
48
|
"author": "Kelvin Omereshone <kelvin@sailscasts.com>",
|
|
24
|
-
"license": "MIT"
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"husky": "^9.0.11",
|
|
52
|
+
"lint-staged": "^15.2.2",
|
|
53
|
+
"mocha": "^11.7.2",
|
|
54
|
+
"prettier": "3.2.5",
|
|
55
|
+
"waterline-adapter-tests": "^1.0.1"
|
|
56
|
+
},
|
|
57
|
+
"lint-staged": {
|
|
58
|
+
"**/*": "prettier --write --ignore-unknown"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"better-sqlite3": "^12.2.0",
|
|
62
|
+
"flaverr": "^1.10.0",
|
|
63
|
+
"machine": "^15.2.3",
|
|
64
|
+
"waterline-utils": "^1.4.5"
|
|
65
|
+
},
|
|
66
|
+
"machinepack": {
|
|
67
|
+
"friendlyName": "SQLite",
|
|
68
|
+
"extendedDescription": "Uses the Node.js better-sqlite3 driver located at https://npmjs.com/package/better-sqlite3",
|
|
69
|
+
"moreInfoUrl": "https://github.com/WiseLibs/better-sqlite3",
|
|
70
|
+
"implements": [
|
|
71
|
+
"connectable",
|
|
72
|
+
"modeled",
|
|
73
|
+
"migratable"
|
|
74
|
+
],
|
|
75
|
+
"machineDir": "lib/private/machines/",
|
|
76
|
+
"exportStyle": "dryDictionary"
|
|
77
|
+
}
|
|
25
78
|
}
|