sails-sqlite 0.1.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/lib/index.js +204 -28
- package/lib/private/build-std-adapter-method.js +8 -4
- package/lib/private/machines/avg-records.js +1 -1
- 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 +1 -1
- package/lib/private/machines/create-each-record.js +1 -1
- package/lib/private/machines/create-record.js +2 -2
- package/lib/private/machines/define-physical-model.js +18 -9
- package/lib/private/machines/destroy-records.js +21 -8
- package/lib/private/machines/drop-physical-model.js +2 -2
- package/lib/private/machines/find-records.js +3 -3
- package/lib/private/machines/join.js +232 -66
- package/lib/private/machines/lease-connection.js +58 -0
- package/lib/private/machines/private/build-sqlite-where-clause.js +10 -8
- package/lib/private/machines/private/compile-statement.js +334 -0
- package/lib/private/machines/private/generate-join-sql-query.js +14 -6
- package/lib/private/machines/private/process-each-record.js +9 -2
- package/lib/private/machines/private/process-native-error.js +85 -40
- package/lib/private/machines/rollback-transaction.js +50 -0
- package/lib/private/machines/sum-records.js +1 -1
- package/lib/private/machines/update-records.js +27 -10
- package/package.json +8 -3
- package/tests/index.js +1 -1
- package/tests/runner.js +99 -0
- package/tests/transaction.test.js +562 -0
- package/tests/adapter.test.js +0 -534
- package/tests/datatypes.test.js +0 -293
- package/tests/sequence.test.js +0 -153
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sails-sqlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "SQLite adapter for Sails/Waterline",
|
|
5
5
|
"main": "lib",
|
|
6
6
|
"directories": {
|
|
7
7
|
"lib": "./lib"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "
|
|
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",
|
|
11
13
|
"lint": "prettier --check .",
|
|
12
14
|
"lint:fix": "prettier --write .",
|
|
13
15
|
"prepare": "husky"
|
|
@@ -48,13 +50,16 @@
|
|
|
48
50
|
"devDependencies": {
|
|
49
51
|
"husky": "^9.0.11",
|
|
50
52
|
"lint-staged": "^15.2.2",
|
|
51
|
-
"
|
|
53
|
+
"mocha": "^11.7.2",
|
|
54
|
+
"prettier": "3.2.5",
|
|
55
|
+
"waterline-adapter-tests": "^1.0.1"
|
|
52
56
|
},
|
|
53
57
|
"lint-staged": {
|
|
54
58
|
"**/*": "prettier --write --ignore-unknown"
|
|
55
59
|
},
|
|
56
60
|
"dependencies": {
|
|
57
61
|
"better-sqlite3": "^12.2.0",
|
|
62
|
+
"flaverr": "^1.10.0",
|
|
58
63
|
"machine": "^15.2.3",
|
|
59
64
|
"waterline-utils": "^1.4.5"
|
|
60
65
|
},
|
package/tests/index.js
CHANGED
package/tests/runner.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run integration tests
|
|
3
|
+
*
|
|
4
|
+
* Uses the `waterline-adapter-tests` module to
|
|
5
|
+
* run mocha tests against the appropriate version
|
|
6
|
+
* of Waterline. Only the interfaces explicitly
|
|
7
|
+
* declared in this adapter's `package.json` file
|
|
8
|
+
* are tested. (e.g. `queryable`, `semantic`, etc.)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Module dependencies
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
var util = require('util')
|
|
16
|
+
var TestRunner = require('waterline-adapter-tests')
|
|
17
|
+
var Adapter = require('../')
|
|
18
|
+
|
|
19
|
+
// Grab targeted interfaces from this adapter's `package.json` file:
|
|
20
|
+
var package = {}
|
|
21
|
+
var interfaces = []
|
|
22
|
+
var features = []
|
|
23
|
+
try {
|
|
24
|
+
package = require('../package.json')
|
|
25
|
+
interfaces = package.waterlineAdapter.interfaces
|
|
26
|
+
features = package.waterlineAdapter.features
|
|
27
|
+
} catch (e) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'\n' +
|
|
30
|
+
'Could not read supported interfaces from `waterlineAdapter.interfaces`' +
|
|
31
|
+
'\n' +
|
|
32
|
+
"in this adapter's `package.json` file ::" +
|
|
33
|
+
'\n' +
|
|
34
|
+
util.inspect(e)
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log('Testing `' + package.name + '`, a Sails/Waterline adapter.')
|
|
39
|
+
console.log(
|
|
40
|
+
'Running `waterline-adapter-tests` against ' +
|
|
41
|
+
interfaces.length +
|
|
42
|
+
' interfaces...'
|
|
43
|
+
)
|
|
44
|
+
console.log('( ' + interfaces.join(', ') + ' )')
|
|
45
|
+
console.log()
|
|
46
|
+
console.log('Latest draft of Waterline adapter interface spec:')
|
|
47
|
+
console.log(
|
|
48
|
+
'http://sailsjs.com/documentation/concepts/extending-sails/adapters'
|
|
49
|
+
)
|
|
50
|
+
console.log()
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Integration Test Runner
|
|
54
|
+
*
|
|
55
|
+
* Uses the `waterline-adapter-tests` module to
|
|
56
|
+
* run mocha tests against the specified interfaces
|
|
57
|
+
* of the currently-implemented Waterline adapter API.
|
|
58
|
+
*/
|
|
59
|
+
new TestRunner({
|
|
60
|
+
// Mocha opts
|
|
61
|
+
mocha: {
|
|
62
|
+
bail: true
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Load the adapter module.
|
|
66
|
+
adapter: Adapter,
|
|
67
|
+
|
|
68
|
+
// Default connection config to use.
|
|
69
|
+
config: {
|
|
70
|
+
url: '.tmp/test-db.sqlite',
|
|
71
|
+
schema: false
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// The set of adapter interfaces to test against.
|
|
75
|
+
// (grabbed these from this adapter's package.json file above)
|
|
76
|
+
interfaces: interfaces,
|
|
77
|
+
|
|
78
|
+
// The set of adapter features to test against.
|
|
79
|
+
// (grabbed these from this adapter's package.json file above)
|
|
80
|
+
features: features
|
|
81
|
+
|
|
82
|
+
// Most databases implement 'semantic' and 'queryable'.
|
|
83
|
+
//
|
|
84
|
+
// As of Sails/Waterline v0.10, the 'associations' interface
|
|
85
|
+
// is also available. If you don't implement 'associations',
|
|
86
|
+
// it will be polyfilled for you by Waterline core. The core
|
|
87
|
+
// implementation will always be used for cross-adapter / cross-connection
|
|
88
|
+
// joins.
|
|
89
|
+
//
|
|
90
|
+
// In future versions of Sails/Waterline, 'queryable' may be also
|
|
91
|
+
// be polyfilled by core.
|
|
92
|
+
//
|
|
93
|
+
// These polyfilled implementations can usually be further optimized at the
|
|
94
|
+
// adapter level, since most databases provide optimizations for internal
|
|
95
|
+
// operations.
|
|
96
|
+
//
|
|
97
|
+
// Full interface reference:
|
|
98
|
+
// https://github.com/balderdashy/sails-docs/blob/master/adapter-specification.md
|
|
99
|
+
})
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
const { test, describe, before, after, beforeEach } = require('node:test')
|
|
2
|
+
const assert = require('node:assert')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
|
|
6
|
+
// Import the adapter
|
|
7
|
+
const adapter = require('../lib/index.js')
|
|
8
|
+
|
|
9
|
+
describe('Transaction support', () => {
|
|
10
|
+
let testDbPath
|
|
11
|
+
let datastore
|
|
12
|
+
let models
|
|
13
|
+
|
|
14
|
+
before(async () => {
|
|
15
|
+
testDbPath = path.join(__dirname, `test-transaction-${Date.now()}.sqlite`)
|
|
16
|
+
datastore = {
|
|
17
|
+
identity: 'testDatastore',
|
|
18
|
+
adapter: 'sails-sqlite',
|
|
19
|
+
url: testDbPath
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
models = {
|
|
23
|
+
user: {
|
|
24
|
+
identity: 'user',
|
|
25
|
+
tableName: 'users',
|
|
26
|
+
primaryKey: 'id',
|
|
27
|
+
definition: {
|
|
28
|
+
id: {
|
|
29
|
+
type: 'number',
|
|
30
|
+
autoIncrement: true,
|
|
31
|
+
columnName: 'id'
|
|
32
|
+
},
|
|
33
|
+
name: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
required: true,
|
|
36
|
+
columnName: 'name'
|
|
37
|
+
},
|
|
38
|
+
email: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
required: true,
|
|
41
|
+
unique: true,
|
|
42
|
+
columnName: 'email'
|
|
43
|
+
},
|
|
44
|
+
balance: {
|
|
45
|
+
type: 'number',
|
|
46
|
+
defaultsTo: 0,
|
|
47
|
+
columnName: 'balance'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Register datastore
|
|
54
|
+
await new Promise((resolve, reject) => {
|
|
55
|
+
adapter.registerDatastore(datastore, models, (err) => {
|
|
56
|
+
if (err) return reject(err)
|
|
57
|
+
resolve()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Create table schema
|
|
62
|
+
const tableDef = {
|
|
63
|
+
id: {
|
|
64
|
+
type: 'number',
|
|
65
|
+
primaryKey: true,
|
|
66
|
+
autoIncrement: true,
|
|
67
|
+
required: true
|
|
68
|
+
},
|
|
69
|
+
name: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
required: true
|
|
72
|
+
},
|
|
73
|
+
email: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
required: true,
|
|
76
|
+
unique: true
|
|
77
|
+
},
|
|
78
|
+
balance: {
|
|
79
|
+
type: 'number',
|
|
80
|
+
defaultsTo: 0
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await new Promise((resolve, reject) => {
|
|
85
|
+
adapter.define('testDatastore', 'users', tableDef, (err) => {
|
|
86
|
+
if (err) return reject(err)
|
|
87
|
+
resolve()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
after(async () => {
|
|
93
|
+
// Teardown datastore
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
adapter.teardown('testDatastore', (err) => {
|
|
96
|
+
if (err) return reject(err)
|
|
97
|
+
resolve()
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Clean up test database
|
|
102
|
+
if (fs.existsSync(testDbPath)) {
|
|
103
|
+
try {
|
|
104
|
+
fs.unlinkSync(testDbPath)
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// Ignore cleanup errors
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
beforeEach(async () => {
|
|
112
|
+
// Clean up any existing data and reset transaction state before each test
|
|
113
|
+
try {
|
|
114
|
+
// First, make sure any active transactions are rolled back
|
|
115
|
+
const connection = await new Promise((resolve, reject) => {
|
|
116
|
+
adapter.leaseConnection('testDatastore', {}, (err, connection) => {
|
|
117
|
+
if (err) return reject(err)
|
|
118
|
+
resolve(connection)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
if (connection && connection.inTransaction) {
|
|
123
|
+
await new Promise((resolve, reject) => {
|
|
124
|
+
adapter.rollbackTransaction(
|
|
125
|
+
'testDatastore',
|
|
126
|
+
{ connection, meta: {} },
|
|
127
|
+
(err) => {
|
|
128
|
+
if (err) return reject(err)
|
|
129
|
+
resolve()
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Clean up existing data
|
|
136
|
+
const findQuery = {
|
|
137
|
+
using: 'users',
|
|
138
|
+
criteria: {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const existingRecords = await new Promise((resolve, reject) => {
|
|
142
|
+
adapter.find('testDatastore', findQuery, (err, result) => {
|
|
143
|
+
if (err) return reject(err)
|
|
144
|
+
resolve(result || [])
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
if (existingRecords.length > 0) {
|
|
149
|
+
// Delete each record by its primary key
|
|
150
|
+
for (const record of existingRecords) {
|
|
151
|
+
const deleteQuery = {
|
|
152
|
+
using: 'users',
|
|
153
|
+
criteria: { id: record.id }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await new Promise((resolve, reject) => {
|
|
157
|
+
adapter.destroy('testDatastore', deleteQuery, (err) => {
|
|
158
|
+
if (err) return reject(err)
|
|
159
|
+
resolve()
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
// Ignore cleanup errors in beforeEach
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('leaseConnection', () => {
|
|
170
|
+
test('should lease a connection successfully', async () => {
|
|
171
|
+
const connection = await new Promise((resolve, reject) => {
|
|
172
|
+
adapter.leaseConnection('testDatastore', {}, (err, connection) => {
|
|
173
|
+
if (err) return reject(err)
|
|
174
|
+
resolve(connection)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
assert(connection, 'Connection should be returned')
|
|
179
|
+
assert(
|
|
180
|
+
typeof connection.inTransaction !== 'undefined',
|
|
181
|
+
'Connection should be a database instance'
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('should fail with invalid datastore name', async () => {
|
|
186
|
+
try {
|
|
187
|
+
await new Promise((resolve, reject) => {
|
|
188
|
+
adapter.leaseConnection('invalidDatastore', {}, (err, connection) => {
|
|
189
|
+
if (err) return reject(err)
|
|
190
|
+
resolve(connection)
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
assert.fail('Should have thrown an error')
|
|
194
|
+
} catch (err) {
|
|
195
|
+
assert(
|
|
196
|
+
err.message.includes('no matching datastore entry'),
|
|
197
|
+
'Should have proper error message'
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe('beginTransaction', () => {
|
|
204
|
+
test('should begin transaction successfully', async () => {
|
|
205
|
+
const connection = await new Promise((resolve, reject) => {
|
|
206
|
+
adapter.leaseConnection('testDatastore', {}, (err, connection) => {
|
|
207
|
+
if (err) return reject(err)
|
|
208
|
+
resolve(connection)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
await new Promise((resolve, reject) => {
|
|
213
|
+
adapter.beginTransaction(
|
|
214
|
+
'testDatastore',
|
|
215
|
+
{ connection, meta: {} },
|
|
216
|
+
(err) => {
|
|
217
|
+
if (err) return reject(err)
|
|
218
|
+
resolve()
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// Verify transaction is active
|
|
224
|
+
assert(connection.inTransaction, 'Transaction should be active')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('should fail to begin nested transaction', async () => {
|
|
228
|
+
const connection = await new Promise((resolve, reject) => {
|
|
229
|
+
adapter.leaseConnection('testDatastore', {}, (err, connection) => {
|
|
230
|
+
if (err) return reject(err)
|
|
231
|
+
resolve(connection)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// Begin first transaction
|
|
236
|
+
await new Promise((resolve, reject) => {
|
|
237
|
+
adapter.beginTransaction(
|
|
238
|
+
'testDatastore',
|
|
239
|
+
{ connection, meta: {} },
|
|
240
|
+
(err) => {
|
|
241
|
+
if (err) return reject(err)
|
|
242
|
+
resolve()
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// Try to begin second transaction (should fail)
|
|
248
|
+
try {
|
|
249
|
+
await new Promise((resolve, reject) => {
|
|
250
|
+
adapter.beginTransaction(
|
|
251
|
+
'testDatastore',
|
|
252
|
+
{ connection, meta: {} },
|
|
253
|
+
(err) => {
|
|
254
|
+
if (err) return reject(err)
|
|
255
|
+
resolve()
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
assert.fail('Should have thrown an error for nested transaction')
|
|
260
|
+
} catch (err) {
|
|
261
|
+
assert(
|
|
262
|
+
err.message.includes('Transaction is already active'),
|
|
263
|
+
'Should have proper error message'
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
describe('commitTransaction', () => {
|
|
270
|
+
test('should commit transaction and persist changes', async () => {
|
|
271
|
+
const connection = await new Promise((resolve, reject) => {
|
|
272
|
+
adapter.leaseConnection('testDatastore', {}, (err, connection) => {
|
|
273
|
+
if (err) return reject(err)
|
|
274
|
+
resolve(connection)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// Begin transaction
|
|
279
|
+
await new Promise((resolve, reject) => {
|
|
280
|
+
adapter.beginTransaction(
|
|
281
|
+
'testDatastore',
|
|
282
|
+
{ connection, meta: {} },
|
|
283
|
+
(err) => {
|
|
284
|
+
if (err) return reject(err)
|
|
285
|
+
resolve()
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
// Create a record within transaction
|
|
291
|
+
const createQuery = {
|
|
292
|
+
using: 'users',
|
|
293
|
+
newRecord: {
|
|
294
|
+
name: 'Transaction User',
|
|
295
|
+
email: 'tx@example.com',
|
|
296
|
+
balance: 100
|
|
297
|
+
},
|
|
298
|
+
meta: { fetch: true }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const createdRecord = await new Promise((resolve, reject) => {
|
|
302
|
+
adapter.create('testDatastore', createQuery, (err, result) => {
|
|
303
|
+
if (err) return reject(err)
|
|
304
|
+
resolve(result)
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
assert(createdRecord, 'Record should be created')
|
|
309
|
+
assert(connection.inTransaction, 'Transaction should still be active')
|
|
310
|
+
|
|
311
|
+
// Commit transaction
|
|
312
|
+
await new Promise((resolve, reject) => {
|
|
313
|
+
adapter.commitTransaction(
|
|
314
|
+
'testDatastore',
|
|
315
|
+
{ connection, meta: {} },
|
|
316
|
+
(err) => {
|
|
317
|
+
if (err) return reject(err)
|
|
318
|
+
resolve()
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
assert(!connection.inTransaction, 'Transaction should be committed')
|
|
324
|
+
|
|
325
|
+
// Verify the record persists after commit
|
|
326
|
+
const findQuery = {
|
|
327
|
+
using: 'users',
|
|
328
|
+
criteria: { email: 'tx@example.com' }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const foundRecords = await new Promise((resolve, reject) => {
|
|
332
|
+
adapter.find('testDatastore', findQuery, (err, result) => {
|
|
333
|
+
if (err) return reject(err)
|
|
334
|
+
resolve(result)
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
assert(foundRecords.length === 1, 'Record should persist after commit')
|
|
339
|
+
assert.equal(foundRecords[0].name, 'Transaction User')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test('should fail to commit when no active transaction', async () => {
|
|
343
|
+
const connection = await new Promise((resolve, reject) => {
|
|
344
|
+
adapter.leaseConnection('testDatastore', {}, (err, connection) => {
|
|
345
|
+
if (err) return reject(err)
|
|
346
|
+
resolve(connection)
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
await new Promise((resolve, reject) => {
|
|
352
|
+
adapter.commitTransaction(
|
|
353
|
+
'testDatastore',
|
|
354
|
+
{ connection, meta: {} },
|
|
355
|
+
(err) => {
|
|
356
|
+
if (err) return reject(err)
|
|
357
|
+
resolve()
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
})
|
|
361
|
+
assert.fail('Should have thrown an error')
|
|
362
|
+
} catch (err) {
|
|
363
|
+
assert(
|
|
364
|
+
err.message.includes('No active transaction'),
|
|
365
|
+
'Should have proper error message'
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
describe('rollbackTransaction', () => {
|
|
372
|
+
test('should rollback transaction and discard changes', async () => {
|
|
373
|
+
const connection = await new Promise((resolve, reject) => {
|
|
374
|
+
adapter.leaseConnection('testDatastore', {}, (err, connection) => {
|
|
375
|
+
if (err) return reject(err)
|
|
376
|
+
resolve(connection)
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// Begin transaction
|
|
381
|
+
await new Promise((resolve, reject) => {
|
|
382
|
+
adapter.beginTransaction(
|
|
383
|
+
'testDatastore',
|
|
384
|
+
{ connection, meta: {} },
|
|
385
|
+
(err) => {
|
|
386
|
+
if (err) return reject(err)
|
|
387
|
+
resolve()
|
|
388
|
+
}
|
|
389
|
+
)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// Create a record within transaction
|
|
393
|
+
const createQuery = {
|
|
394
|
+
using: 'users',
|
|
395
|
+
newRecord: {
|
|
396
|
+
name: 'Rollback User',
|
|
397
|
+
email: 'rollback@example.com',
|
|
398
|
+
balance: 500
|
|
399
|
+
},
|
|
400
|
+
meta: { fetch: true }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const createdRecord = await new Promise((resolve, reject) => {
|
|
404
|
+
adapter.create('testDatastore', createQuery, (err, result) => {
|
|
405
|
+
if (err) return reject(err)
|
|
406
|
+
resolve(result)
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
assert(createdRecord, 'Record should be created')
|
|
411
|
+
assert(connection.inTransaction, 'Transaction should be active')
|
|
412
|
+
|
|
413
|
+
// Rollback transaction
|
|
414
|
+
await new Promise((resolve, reject) => {
|
|
415
|
+
adapter.rollbackTransaction(
|
|
416
|
+
'testDatastore',
|
|
417
|
+
{ connection, meta: {} },
|
|
418
|
+
(err) => {
|
|
419
|
+
if (err) return reject(err)
|
|
420
|
+
resolve()
|
|
421
|
+
}
|
|
422
|
+
)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
assert(!connection.inTransaction, 'Transaction should be rolled back')
|
|
426
|
+
|
|
427
|
+
// Verify the record does NOT persist after rollback
|
|
428
|
+
const findQuery = {
|
|
429
|
+
using: 'users',
|
|
430
|
+
criteria: { email: 'rollback@example.com' }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const foundRecords = await new Promise((resolve, reject) => {
|
|
434
|
+
adapter.find('testDatastore', findQuery, (err, result) => {
|
|
435
|
+
if (err) return reject(err)
|
|
436
|
+
resolve(result)
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
assert(
|
|
441
|
+
foundRecords.length === 0,
|
|
442
|
+
'Record should NOT persist after rollback'
|
|
443
|
+
)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
test('should fail to rollback when no active transaction', async () => {
|
|
447
|
+
const connection = await new Promise((resolve, reject) => {
|
|
448
|
+
adapter.leaseConnection('testDatastore', {}, (err, connection) => {
|
|
449
|
+
if (err) return reject(err)
|
|
450
|
+
resolve(connection)
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
await new Promise((resolve, reject) => {
|
|
456
|
+
adapter.rollbackTransaction(
|
|
457
|
+
'testDatastore',
|
|
458
|
+
{ connection, meta: {} },
|
|
459
|
+
(err) => {
|
|
460
|
+
if (err) return reject(err)
|
|
461
|
+
resolve()
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
})
|
|
465
|
+
assert.fail('Should have thrown an error')
|
|
466
|
+
} catch (err) {
|
|
467
|
+
assert(
|
|
468
|
+
err.message.includes('No active transaction'),
|
|
469
|
+
'Should have proper error message'
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
describe('Transaction isolation', () => {
|
|
476
|
+
test('should maintain transaction isolation', async () => {
|
|
477
|
+
// Create initial record
|
|
478
|
+
const initialQuery = {
|
|
479
|
+
using: 'users',
|
|
480
|
+
newRecord: {
|
|
481
|
+
name: 'Initial User',
|
|
482
|
+
email: 'initial@example.com',
|
|
483
|
+
balance: 1000
|
|
484
|
+
},
|
|
485
|
+
meta: { fetch: true }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await new Promise((resolve, reject) => {
|
|
489
|
+
adapter.create('testDatastore', initialQuery, (err, result) => {
|
|
490
|
+
if (err) return reject(err)
|
|
491
|
+
resolve(result)
|
|
492
|
+
})
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
const connection = await new Promise((resolve, reject) => {
|
|
496
|
+
adapter.leaseConnection('testDatastore', {}, (err, connection) => {
|
|
497
|
+
if (err) return reject(err)
|
|
498
|
+
resolve(connection)
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
// Begin transaction
|
|
503
|
+
await new Promise((resolve, reject) => {
|
|
504
|
+
adapter.beginTransaction(
|
|
505
|
+
'testDatastore',
|
|
506
|
+
{ connection, meta: {} },
|
|
507
|
+
(err) => {
|
|
508
|
+
if (err) return reject(err)
|
|
509
|
+
resolve()
|
|
510
|
+
}
|
|
511
|
+
)
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
// Update record within transaction
|
|
515
|
+
const updateQuery = {
|
|
516
|
+
using: 'users',
|
|
517
|
+
criteria: { email: 'initial@example.com' },
|
|
518
|
+
valuesToSet: { balance: 500 },
|
|
519
|
+
meta: { fetch: true }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
await new Promise((resolve, reject) => {
|
|
523
|
+
adapter.update('testDatastore', updateQuery, (err, result) => {
|
|
524
|
+
if (err) return reject(err)
|
|
525
|
+
resolve(result)
|
|
526
|
+
})
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
// Rollback the transaction
|
|
530
|
+
await new Promise((resolve, reject) => {
|
|
531
|
+
adapter.rollbackTransaction(
|
|
532
|
+
'testDatastore',
|
|
533
|
+
{ connection, meta: {} },
|
|
534
|
+
(err) => {
|
|
535
|
+
if (err) return reject(err)
|
|
536
|
+
resolve()
|
|
537
|
+
}
|
|
538
|
+
)
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
// Verify original value is restored
|
|
542
|
+
const findQuery = {
|
|
543
|
+
using: 'users',
|
|
544
|
+
criteria: { email: 'initial@example.com' }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const foundRecords = await new Promise((resolve, reject) => {
|
|
548
|
+
adapter.find('testDatastore', findQuery, (err, result) => {
|
|
549
|
+
if (err) return reject(err)
|
|
550
|
+
resolve(result)
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
assert(foundRecords.length === 1, 'Record should exist')
|
|
555
|
+
assert.equal(
|
|
556
|
+
foundRecords[0].balance,
|
|
557
|
+
1000,
|
|
558
|
+
'Original balance should be restored'
|
|
559
|
+
)
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
})
|