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,126 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Create (record)',
|
|
3
|
+
|
|
4
|
+
description: 'Create a new physical record in the SQLite database.',
|
|
5
|
+
|
|
6
|
+
inputs: {
|
|
7
|
+
query: require('../constants/query.input'),
|
|
8
|
+
connection: require('../constants/connection.input'),
|
|
9
|
+
dryOrm: require('../constants/dry-orm.input')
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
exits: {
|
|
13
|
+
success: {
|
|
14
|
+
outputFriendlyName: 'Record (maybe)',
|
|
15
|
+
outputDescription:
|
|
16
|
+
'Either `null` or (if `fetch:true`) a dictionary representing the new record that was created.',
|
|
17
|
+
outputExample: '==='
|
|
18
|
+
},
|
|
19
|
+
notUnique: require('../constants/not-unique.exit')
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
fn: function (inputs, exits) {
|
|
23
|
+
// Dependencies
|
|
24
|
+
const util = require('util')
|
|
25
|
+
const _ = require('@sailshq/lodash')
|
|
26
|
+
const processNativeRecord = require('./private/process-native-record')
|
|
27
|
+
const processNativeError = require('./private/process-native-error')
|
|
28
|
+
const reifyValuesToSet = require('./private/reify-values-to-set')
|
|
29
|
+
|
|
30
|
+
// Local var for the stage 3 query, for easier access.
|
|
31
|
+
const s3q = inputs.query
|
|
32
|
+
if (s3q.meta && s3q.meta.logSQLiteS3Qs) {
|
|
33
|
+
console.log(
|
|
34
|
+
'* * * * * *\nADAPTER (CREATE RECORD):',
|
|
35
|
+
util.inspect(s3q, { depth: 5 }),
|
|
36
|
+
'\n'
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Local var for the `tableName`, for clarity.
|
|
41
|
+
const tableName = s3q.using
|
|
42
|
+
|
|
43
|
+
// Grab the model definition
|
|
44
|
+
const WLModel = _.find(inputs.dryOrm.models, { tableName: tableName })
|
|
45
|
+
if (!WLModel) {
|
|
46
|
+
return exits.error(
|
|
47
|
+
new Error(
|
|
48
|
+
`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.)`
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Reify values to set
|
|
54
|
+
try {
|
|
55
|
+
reifyValuesToSet(s3q.newRecord, WLModel, s3q.meta)
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return exits.error(e)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Determine whether to fetch or not
|
|
61
|
+
const isFetchEnabled = !!(s3q.meta && s3q.meta.fetch)
|
|
62
|
+
|
|
63
|
+
// Create this new record in the SQLite database
|
|
64
|
+
const db = inputs.connection
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Build column names and values arrays
|
|
68
|
+
const columnNames = Object.keys(s3q.newRecord)
|
|
69
|
+
const columnValues = Object.values(s3q.newRecord)
|
|
70
|
+
|
|
71
|
+
// Validate that we have data to insert
|
|
72
|
+
if (columnNames.length === 0) {
|
|
73
|
+
throw new Error('Cannot create record: no data provided')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Prepare the INSERT statement with proper SQL escaping
|
|
77
|
+
const columns = columnNames.join(', ')
|
|
78
|
+
const placeholders = columnNames.map(() => '?').join(', ')
|
|
79
|
+
const sql = `INSERT INTO \`${tableName}\` (${columns}) VALUES (${placeholders})`
|
|
80
|
+
|
|
81
|
+
// Use prepared statement (optimized for repeated use)
|
|
82
|
+
const stmt = db.getPreparedStatement
|
|
83
|
+
? db.getPreparedStatement(sql)
|
|
84
|
+
: db.prepare(sql)
|
|
85
|
+
|
|
86
|
+
// Execute the INSERT statement within a transaction for consistency
|
|
87
|
+
const info = db.runInTransaction
|
|
88
|
+
? db.runInTransaction(() => stmt.run(columnValues))
|
|
89
|
+
: stmt.run(columnValues)
|
|
90
|
+
|
|
91
|
+
// If `fetch` is NOT enabled, we're done.
|
|
92
|
+
if (!isFetchEnabled) {
|
|
93
|
+
return exits.success()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Otherwise, fetch the newly created record
|
|
97
|
+
const selectSql = `SELECT * FROM ${tableName} WHERE rowid = ?`
|
|
98
|
+
const selectStmt = db.prepare(selectSql)
|
|
99
|
+
const phRecord = selectStmt.get(info.lastInsertRowid)
|
|
100
|
+
|
|
101
|
+
if (!phRecord) {
|
|
102
|
+
return exits.error(
|
|
103
|
+
new Error(
|
|
104
|
+
'Consistency violation: Unable to retrieve the inserted record. This might indicate a consistency violation.'
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Process record (mutate in-place) to wash away adapter-specific eccentricities.
|
|
111
|
+
processNativeRecord(phRecord, WLModel, s3q.meta)
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return exits.error(e)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Send back the record
|
|
117
|
+
return exits.success(phRecord)
|
|
118
|
+
} catch (err) {
|
|
119
|
+
err = processNativeError(err)
|
|
120
|
+
if (err.footprint && err.footprint.identity === 'notUnique') {
|
|
121
|
+
return exits.notUnique(err)
|
|
122
|
+
}
|
|
123
|
+
return exits.error(err)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Define (physical model)',
|
|
3
|
+
|
|
4
|
+
description:
|
|
5
|
+
'Define a physical model (i.e. SQLite table) with the specified characteristics, creating indexes as needed.',
|
|
6
|
+
|
|
7
|
+
sideEffects: 'idempotent',
|
|
8
|
+
|
|
9
|
+
inputs: {
|
|
10
|
+
connection: require('../constants/connection.input'),
|
|
11
|
+
tableName: require('../constants/table-name.input'),
|
|
12
|
+
columns: {
|
|
13
|
+
description: 'An array of column definitions.',
|
|
14
|
+
required: true,
|
|
15
|
+
example: '==='
|
|
16
|
+
},
|
|
17
|
+
meta: require('../constants/meta.input')
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
exits: {
|
|
21
|
+
success: {
|
|
22
|
+
description:
|
|
23
|
+
'New physical model (and any necessary indexes) were created successfully.'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
fn: function (inputs, exits) {
|
|
28
|
+
const db = inputs.connection
|
|
29
|
+
function getSqliteType(columnType) {
|
|
30
|
+
if (!columnType || typeof columnType !== 'string') {
|
|
31
|
+
return 'TEXT' // Default fallback
|
|
32
|
+
}
|
|
33
|
+
switch (columnType.toLowerCase()) {
|
|
34
|
+
case '_string':
|
|
35
|
+
case '_text':
|
|
36
|
+
case '_mediumtext':
|
|
37
|
+
case '_longtext':
|
|
38
|
+
return 'TEXT'
|
|
39
|
+
case '_number':
|
|
40
|
+
case '_numberkey':
|
|
41
|
+
case '_numbertimestamp':
|
|
42
|
+
case 'integer':
|
|
43
|
+
case 'int':
|
|
44
|
+
return 'INTEGER'
|
|
45
|
+
case '_json':
|
|
46
|
+
return 'TEXT'
|
|
47
|
+
case 'float':
|
|
48
|
+
case 'double':
|
|
49
|
+
case 'real':
|
|
50
|
+
return 'REAL'
|
|
51
|
+
case 'boolean':
|
|
52
|
+
return 'INTEGER'
|
|
53
|
+
case 'date':
|
|
54
|
+
case 'datetime':
|
|
55
|
+
return 'TEXT'
|
|
56
|
+
case 'binary':
|
|
57
|
+
case 'blob':
|
|
58
|
+
return 'BLOB'
|
|
59
|
+
default:
|
|
60
|
+
return 'TEXT'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Start a transaction
|
|
66
|
+
db.prepare('BEGIN').run()
|
|
67
|
+
|
|
68
|
+
// Build and execute the CREATE TABLE statement
|
|
69
|
+
let createTableSQL = `CREATE TABLE IF NOT EXISTS ${inputs.tableName} (`
|
|
70
|
+
let columnDefs = inputs.columns.map((column) => {
|
|
71
|
+
const columnType = column.columnType ?? column.type
|
|
72
|
+
let def = `${column.columnName} ${column.autoIncrement ? 'INTEGER' : getSqliteType(columnType)}`
|
|
73
|
+
if (column.autoIncrement) {
|
|
74
|
+
def += ' PRIMARY KEY AUTOINCREMENT NOT NULL'
|
|
75
|
+
}
|
|
76
|
+
if (column.unique && !column.autoIncrement) def += ' UNIQUE'
|
|
77
|
+
return def
|
|
78
|
+
})
|
|
79
|
+
createTableSQL += columnDefs.join(', ') + ')'
|
|
80
|
+
db.prepare(createTableSQL).run()
|
|
81
|
+
|
|
82
|
+
// Create indexes
|
|
83
|
+
inputs.columns.forEach((column) => {
|
|
84
|
+
if (column.unique && !column.autoIncrement) {
|
|
85
|
+
const indexSQL = `CREATE UNIQUE INDEX IF NOT EXISTS idx_${inputs.tableName}_${column.columnName} ON ${inputs.tableName} (${column.columnName})`
|
|
86
|
+
db.prepare(indexSQL).run()
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Commit the transaction
|
|
91
|
+
db.prepare('COMMIT').run()
|
|
92
|
+
|
|
93
|
+
return exits.success()
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// If there's an error, roll back the transaction
|
|
96
|
+
db.prepare('ROLLBACK').run()
|
|
97
|
+
return exits.error(
|
|
98
|
+
new Error(`Error defining table ${inputs.tableName}: ${error.message}`)
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Destroy manager',
|
|
3
|
+
|
|
4
|
+
description: 'Destroy the specified SQLite connection manager.',
|
|
5
|
+
|
|
6
|
+
extendedDescription:
|
|
7
|
+
'For SQLite, this involves closing the database connection. Unlike other databases, SQLite does not use connection pools, so this operation is relatively straightforward.',
|
|
8
|
+
|
|
9
|
+
sync: true,
|
|
10
|
+
|
|
11
|
+
inputs: {
|
|
12
|
+
manager: {
|
|
13
|
+
description: 'The SQLite connection manager instance to destroy.',
|
|
14
|
+
extendedDescription:
|
|
15
|
+
'Only managers built using the `createManager()` method of this driver are supported. The database connection manager instance provided must not have been destroyed previously.',
|
|
16
|
+
example: '===',
|
|
17
|
+
required: true
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
meta: {
|
|
21
|
+
friendlyName: 'Meta (custom)',
|
|
22
|
+
description: 'Additional options to pass to the SQLite driver.',
|
|
23
|
+
extendedDescription:
|
|
24
|
+
'This is reserved for custom driver-specific extensions. Please refer to the better-sqlite3 documentation for more specific information.',
|
|
25
|
+
example: '==='
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
exits: {
|
|
30
|
+
success: {
|
|
31
|
+
description: 'The specified SQLite manager was successfully destroyed.',
|
|
32
|
+
outputFriendlyName: 'Report',
|
|
33
|
+
outputDescription:
|
|
34
|
+
'The `meta` property is reserved for custom driver-specific extensions.',
|
|
35
|
+
outputExample: '==='
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
fn: ({ manager, meta }, exits) => {
|
|
40
|
+
try {
|
|
41
|
+
// Validate the manager
|
|
42
|
+
if (
|
|
43
|
+
typeof manager !== 'object' ||
|
|
44
|
+
manager === null ||
|
|
45
|
+
typeof manager.close !== 'function'
|
|
46
|
+
) {
|
|
47
|
+
return exits.error(
|
|
48
|
+
new Error(
|
|
49
|
+
'The provided `manager` is not a valid SQLite manager. It should be a better-sqlite3 Database instance with a `close` method.'
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if the database is already closed
|
|
55
|
+
if (!manager.open) {
|
|
56
|
+
console.warn(
|
|
57
|
+
'SQLite manager appears to already be closed, skipping destruction'
|
|
58
|
+
)
|
|
59
|
+
return exits.success({ meta })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Use graceful cleanup if available (from enhanced create-manager)
|
|
63
|
+
if (typeof manager.closeGracefully === 'function') {
|
|
64
|
+
manager.closeGracefully()
|
|
65
|
+
} else {
|
|
66
|
+
// Fallback to basic close
|
|
67
|
+
manager.close()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Verify the connection is actually closed
|
|
71
|
+
if (manager.open) {
|
|
72
|
+
throw new Error('Failed to close SQLite database connection')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return exits.success({
|
|
76
|
+
meta: {
|
|
77
|
+
...meta,
|
|
78
|
+
destroyedAt: new Date().toISOString()
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return exits.error(
|
|
83
|
+
new Error(`Error destroying SQLite manager: ${error.message}`)
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const util = require('util')
|
|
2
|
+
const processNativeRecord = require('./private/process-native-record')
|
|
3
|
+
const buildSqliteWhereClause = require('./private/build-sqlite-where-clause')
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
friendlyName: 'Destroy (records)',
|
|
7
|
+
|
|
8
|
+
description:
|
|
9
|
+
'Destroy record(s) in the SQLite database matching a query criteria.',
|
|
10
|
+
|
|
11
|
+
inputs: {
|
|
12
|
+
query: require('../constants/query.input'),
|
|
13
|
+
connection: require('../constants/connection.input'),
|
|
14
|
+
dryOrm: require('../constants/dry-orm.input')
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
exits: {
|
|
18
|
+
success: {
|
|
19
|
+
outputFriendlyName: 'Records (maybe)',
|
|
20
|
+
outputDescription:
|
|
21
|
+
'Either `null` OR (if `fetch:true`) an array of physical records that were destroyed.',
|
|
22
|
+
outputExample: '==='
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
fn: async function (inputs, exits) {
|
|
27
|
+
const s3q = inputs.query
|
|
28
|
+
if (s3q.meta && s3q.meta.logSqliteS3Qs) {
|
|
29
|
+
console.log(
|
|
30
|
+
'* * * * * *\nADAPTER (DESTROY RECORDS):',
|
|
31
|
+
util.inspect(s3q, { depth: 5 }),
|
|
32
|
+
'\n'
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tableName = s3q.using
|
|
37
|
+
// Find model by tableName since models is an object, not an array
|
|
38
|
+
let WLModel = null
|
|
39
|
+
for (const modelIdentity in inputs.dryOrm.models) {
|
|
40
|
+
if (inputs.dryOrm.models[modelIdentity].tableName === tableName) {
|
|
41
|
+
WLModel = inputs.dryOrm.models[modelIdentity]
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!WLModel) {
|
|
47
|
+
return exits.error(
|
|
48
|
+
new Error(
|
|
49
|
+
`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.)`
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const pkColumnName = WLModel.attributes[WLModel.primaryKey].columnName
|
|
55
|
+
const isFetchEnabled = !!(s3q.meta && s3q.meta.fetch)
|
|
56
|
+
|
|
57
|
+
const sqliteWhere = buildSqliteWhereClause(
|
|
58
|
+
s3q.criteria.where,
|
|
59
|
+
WLModel,
|
|
60
|
+
s3q.meta
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const db = inputs.connection
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Start a transaction
|
|
67
|
+
db.exec('BEGIN TRANSACTION')
|
|
68
|
+
|
|
69
|
+
let phRecords
|
|
70
|
+
if (isFetchEnabled) {
|
|
71
|
+
// Fetch matching records before deletion
|
|
72
|
+
const selectSql = `SELECT * FROM ${tableName} WHERE ${sqliteWhere}`
|
|
73
|
+
const selectStmt = db.prepare(selectSql)
|
|
74
|
+
phRecords = selectStmt.all()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Perform the deletion
|
|
78
|
+
const deleteSql = `DELETE FROM ${tableName} WHERE ${sqliteWhere}`
|
|
79
|
+
const deleteStmt = db.prepare(deleteSql)
|
|
80
|
+
const deleteInfo = deleteStmt.run()
|
|
81
|
+
|
|
82
|
+
// Commit the transaction
|
|
83
|
+
db.exec('COMMIT')
|
|
84
|
+
|
|
85
|
+
if (!isFetchEnabled) {
|
|
86
|
+
return exits.success()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Process fetched records
|
|
90
|
+
phRecords.forEach((phRecord) => {
|
|
91
|
+
processNativeRecord(phRecord, WLModel, s3q.meta)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return exits.success(phRecords)
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Rollback the transaction in case of error
|
|
97
|
+
db.exec('ROLLBACK')
|
|
98
|
+
return exits.error(err)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Drop (physical model)',
|
|
3
|
+
|
|
4
|
+
description:
|
|
5
|
+
'Completely drop & destroy any traces of a particular physical model (i.e. SQLite table).',
|
|
6
|
+
|
|
7
|
+
sideEffects: 'idempotent',
|
|
8
|
+
|
|
9
|
+
inputs: {
|
|
10
|
+
connection: require('../constants/connection.input'),
|
|
11
|
+
tableName: require('../constants/table-name.input'),
|
|
12
|
+
meta: require('../constants/meta.input')
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
exits: {
|
|
16
|
+
success: {
|
|
17
|
+
description:
|
|
18
|
+
'If such a physical model exists, it was dropped successfully.'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
fn: function (inputs, exits) {
|
|
23
|
+
// Get the SQLite database connection
|
|
24
|
+
const db = inputs.connection
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// SQL to drop the table
|
|
28
|
+
const dropTableSQL = `DROP TABLE IF EXISTS ${inputs.tableName}`
|
|
29
|
+
|
|
30
|
+
// Execute the drop table operation
|
|
31
|
+
db.prepare(dropTableSQL).run()
|
|
32
|
+
|
|
33
|
+
// SQL to remove the table's entry from sqlite_sequence (if it exists)
|
|
34
|
+
const cleanSequenceSQL = `DELETE FROM sqlite_sequence WHERE name = ?`
|
|
35
|
+
|
|
36
|
+
// Clean up the sqlite_sequence
|
|
37
|
+
db.prepare(cleanSequenceSQL).run(inputs.tableName)
|
|
38
|
+
|
|
39
|
+
// Return success, as the main operation (dropping the table) was successful
|
|
40
|
+
return exits.success()
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.message.includes('no such table: sqlite_sequence')) {
|
|
43
|
+
// If sqlite_sequence doesn't exist, it's not an error - just means no autoincrement was used
|
|
44
|
+
return exits.success()
|
|
45
|
+
}
|
|
46
|
+
return exits.error(
|
|
47
|
+
new Error(`Error dropping table ${inputs.tableName}: ${error.message}`)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const assert = require('assert')
|
|
2
|
+
const util = require('util')
|
|
3
|
+
const processNativeRecord = require('./private/process-native-record')
|
|
4
|
+
const buildSqliteWhereClause = require('./private/build-sqlite-where-clause')
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
friendlyName: 'Find (records)',
|
|
8
|
+
|
|
9
|
+
description: 'Find record(s) in the SQLite database.',
|
|
10
|
+
|
|
11
|
+
inputs: {
|
|
12
|
+
query: require('../constants/query.input'),
|
|
13
|
+
connection: require('../constants/connection.input'),
|
|
14
|
+
dryOrm: require('../constants/dry-orm.input')
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
exits: {
|
|
18
|
+
success: {
|
|
19
|
+
outputFriendlyName: 'Records',
|
|
20
|
+
outputDescription: 'An array of physical records.',
|
|
21
|
+
outputExample: '===' //[ {===} ]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
fn: async function (inputs, exits) {
|
|
26
|
+
const s3q = inputs.query
|
|
27
|
+
if (s3q.meta && s3q.meta.logSqliteS3Qs) {
|
|
28
|
+
console.log(
|
|
29
|
+
'* * * * * *\nADAPTER (FIND RECORDS):',
|
|
30
|
+
util.inspect(s3q, { depth: 10 }),
|
|
31
|
+
'\n'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const tableName = s3q.using
|
|
36
|
+
// Find model by tableName since models is an object, not an array
|
|
37
|
+
let WLModel = null
|
|
38
|
+
for (const modelIdentity in inputs.dryOrm.models) {
|
|
39
|
+
if (inputs.dryOrm.models[modelIdentity].tableName === tableName) {
|
|
40
|
+
WLModel = inputs.dryOrm.models[modelIdentity]
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!WLModel) {
|
|
46
|
+
return exits.error(
|
|
47
|
+
new Error(
|
|
48
|
+
`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.)`
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const db = inputs.connection
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
let sqlQuery = `SELECT `
|
|
57
|
+
|
|
58
|
+
// Handle SELECT clause
|
|
59
|
+
if (s3q.criteria.select) {
|
|
60
|
+
sqlQuery += s3q.criteria.select.join(', ')
|
|
61
|
+
} else {
|
|
62
|
+
sqlQuery += '*'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
sqlQuery += ` FROM ${tableName}`
|
|
66
|
+
|
|
67
|
+
// Handle WHERE clause
|
|
68
|
+
const whereClause = buildSqliteWhereClause(
|
|
69
|
+
s3q.criteria.where,
|
|
70
|
+
WLModel,
|
|
71
|
+
s3q.meta
|
|
72
|
+
)
|
|
73
|
+
if (whereClause) {
|
|
74
|
+
sqlQuery += ` WHERE ${whereClause}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle SORT clause
|
|
78
|
+
if (s3q.criteria.sort && s3q.criteria.sort.length) {
|
|
79
|
+
const sortClauses = s3q.criteria.sort.map((sortObj) => {
|
|
80
|
+
const key = Object.keys(sortObj)[0]
|
|
81
|
+
const direction = sortObj[key] === 'ASC' ? 'ASC' : 'DESC'
|
|
82
|
+
return `${key} ${direction}`
|
|
83
|
+
})
|
|
84
|
+
sqlQuery += ` ORDER BY ${sortClauses.join(', ')}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle LIMIT clause
|
|
88
|
+
// If no limit is specified, don't add LIMIT clause (will return all matching records)
|
|
89
|
+
// This handles cases where Waterline doesn't provide a default limit
|
|
90
|
+
if (
|
|
91
|
+
s3q.criteria.limit !== undefined &&
|
|
92
|
+
Number.isFinite(s3q.criteria.limit)
|
|
93
|
+
) {
|
|
94
|
+
sqlQuery += ` LIMIT ${s3q.criteria.limit}`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle SKIP (OFFSET) clause
|
|
98
|
+
if (s3q.criteria.skip) {
|
|
99
|
+
sqlQuery += ` OFFSET ${s3q.criteria.skip}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Use prepared statement caching for better performance
|
|
103
|
+
const stmt = db.getPreparedStatement
|
|
104
|
+
? db.getPreparedStatement(sqlQuery)
|
|
105
|
+
: db.prepare(sqlQuery)
|
|
106
|
+
|
|
107
|
+
const nativeResult = stmt.all()
|
|
108
|
+
|
|
109
|
+
// Process records
|
|
110
|
+
const phRecords = nativeResult.map((record) => {
|
|
111
|
+
processNativeRecord(record, WLModel, s3q.meta)
|
|
112
|
+
return record
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return exits.success(phRecords)
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return exits.error(err)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Get connection',
|
|
3
|
+
|
|
4
|
+
description:
|
|
5
|
+
'Get an active connection to the SQLite database (this is a no-op for SQLite).',
|
|
6
|
+
|
|
7
|
+
moreInfoUrl:
|
|
8
|
+
'https://github.com/node-machine/driver-interface/blob/master/machines/get-connection.js',
|
|
9
|
+
|
|
10
|
+
sync: true,
|
|
11
|
+
|
|
12
|
+
inputs: {
|
|
13
|
+
manager: {
|
|
14
|
+
description: 'A SQLite database instance (from better-sqlite3).',
|
|
15
|
+
example: '===',
|
|
16
|
+
required: true
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
meta: {
|
|
20
|
+
friendlyName: 'Meta (unused)',
|
|
21
|
+
description: 'Additional stuff to pass to the driver.',
|
|
22
|
+
example: '==='
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
exits: {
|
|
27
|
+
success: {
|
|
28
|
+
outputFriendlyName: 'Report',
|
|
29
|
+
outputDescription:
|
|
30
|
+
'The `connection` property is a SQLite database instance. The `meta` property is unused.',
|
|
31
|
+
outputExample: '==='
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
failed: {
|
|
35
|
+
friendlyName: 'Failed (unused)',
|
|
36
|
+
description:
|
|
37
|
+
'Could not acquire a connection to the database via the provided connection manager. (This is unlikely to occur with SQLite)',
|
|
38
|
+
outputFriendlyName: 'Report',
|
|
39
|
+
outputExample: {
|
|
40
|
+
error: '===',
|
|
41
|
+
meta: '==='
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
fn: ({ manager, meta }, exits) => {
|
|
47
|
+
// This is a no-op that just sends back the manager and `meta` that were passed in.
|
|
48
|
+
// In SQLite, the "manager" and "connection" are the same thing: a Database instance from better-sqlite3.
|
|
49
|
+
return exits.success({
|
|
50
|
+
connection: manager,
|
|
51
|
+
meta
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|