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.
Files changed (29) hide show
  1. package/lib/index.js +204 -28
  2. package/lib/private/build-std-adapter-method.js +8 -4
  3. package/lib/private/machines/avg-records.js +1 -1
  4. package/lib/private/machines/begin-transaction.js +51 -0
  5. package/lib/private/machines/commit-transaction.js +50 -0
  6. package/lib/private/machines/count-records.js +1 -1
  7. package/lib/private/machines/create-each-record.js +1 -1
  8. package/lib/private/machines/create-record.js +2 -2
  9. package/lib/private/machines/define-physical-model.js +18 -9
  10. package/lib/private/machines/destroy-records.js +21 -8
  11. package/lib/private/machines/drop-physical-model.js +2 -2
  12. package/lib/private/machines/find-records.js +3 -3
  13. package/lib/private/machines/join.js +232 -66
  14. package/lib/private/machines/lease-connection.js +58 -0
  15. package/lib/private/machines/private/build-sqlite-where-clause.js +10 -8
  16. package/lib/private/machines/private/compile-statement.js +334 -0
  17. package/lib/private/machines/private/generate-join-sql-query.js +14 -6
  18. package/lib/private/machines/private/process-each-record.js +9 -2
  19. package/lib/private/machines/private/process-native-error.js +85 -40
  20. package/lib/private/machines/rollback-transaction.js +50 -0
  21. package/lib/private/machines/sum-records.js +1 -1
  22. package/lib/private/machines/update-records.js +27 -10
  23. package/package.json +8 -3
  24. package/tests/index.js +1 -1
  25. package/tests/runner.js +99 -0
  26. package/tests/transaction.test.js +562 -0
  27. package/tests/adapter.test.js +0 -534
  28. package/tests/datatypes.test.js +0 -293
  29. 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.1.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": "node 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",
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
- "prettier": "3.2.5"
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
@@ -6,7 +6,7 @@ const fs = require('node:fs')
6
6
 
7
7
  // __dirname is automatically available in CommonJS
8
8
 
9
- const testFiles = ['adapter.test.js', 'sequence.test.js', 'datatypes.test.js']
9
+ const testFiles = ['transaction.test.js']
10
10
 
11
11
  function cleanupTestDatabases() {
12
12
  try {
@@ -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
+ })