@quatrain/backend-sqlite 1.0.2 → 1.0.4

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.
@@ -0,0 +1,67 @@
1
+ import { User } from '@quatrain/backend'
2
+ import { createUser, setup } from './common'
3
+
4
+ const adapter = setup()
5
+ let user: User
6
+
7
+ beforeAll(async () => {
8
+ user = await createUser()
9
+ await adapter.create(user.dataObject, undefined)
10
+ })
11
+
12
+ afterAll(async () => {
13
+ await adapter.deleteCollection('user')
14
+ await adapter.close()
15
+ })
16
+
17
+ describe('SQLite CRUD operations', () => {
18
+ test('write data', async () => {
19
+ // Check that object is successfully created in backend
20
+ expect(user.dataObject.isPersisted(false)).toBe(true)
21
+ expect(user.uri).not.toBeUndefined()
22
+ expect(user.uid).not.toBeUndefined()
23
+ expect(user.uri.constructor.name).toBe('ObjectUri')
24
+ })
25
+
26
+ test('read data', async () => {
27
+ // Retrieve user from empty object and record path
28
+ const user2 = await User.factory()
29
+ user2.dataObject.uri.path = user.dataObject.uri.path
30
+
31
+ await adapter.read(user2.dataObject)
32
+ expect(user2._.firstname).toBe(user._.firstname)
33
+ expect(user2._.lastname).toBe(user._.lastname)
34
+ expect(user2._.email).toBe(user._.email)
35
+ })
36
+
37
+ test('update data', async () => {
38
+ user._.firstname = 'Jane'
39
+ await adapter.update(user.dataObject)
40
+
41
+ // Retrieve user from empty object and record path
42
+ const user2 = await User.factory()
43
+ user2.dataObject.uri.path = user.dataObject.uri.path
44
+
45
+ await adapter.read(user2.dataObject)
46
+ expect(user2._.firstname).toBe('Jane')
47
+ })
48
+
49
+ test('soft delete data', async () => {
50
+ expect(user.uid).toBeDefined()
51
+
52
+ await adapter.delete(user.dataObject, false)
53
+ expect(user.uid).toBeUndefined()
54
+ })
55
+
56
+ test('hard delete data', async () => {
57
+ // Create a new user for hard delete test
58
+ const deleteUser = await createUser()
59
+ deleteUser._.email = 'delete@test.com'
60
+ await adapter.create(deleteUser.dataObject, undefined)
61
+
62
+ expect(deleteUser.uid).toBeDefined()
63
+
64
+ await adapter.delete(deleteUser.dataObject, true)
65
+ expect(deleteUser.uid).toBeUndefined()
66
+ })
67
+ })
@@ -0,0 +1,433 @@
1
+ import { User } from '@quatrain/backend'
2
+ import { BackendError, Filter, OperatorKeys } from '@quatrain/backend'
3
+ import { NotFoundError } from '@quatrain/core'
4
+ import { SQLiteAdapter } from '../SQLiteAdapter'
5
+ import { createUser, createEntity, setup, Entity } from './common'
6
+ import fs from 'fs'
7
+ import path from 'path'
8
+
9
+ describe('SQLiteAdapter Tests', () => {
10
+ let adapter: SQLiteAdapter
11
+ let user: User
12
+ let entity: Entity
13
+ const testDbPath = path.join(__dirname, 'test.db')
14
+
15
+ beforeAll(async () => {
16
+ // Clean up any existing test database
17
+ if (fs.existsSync(testDbPath)) {
18
+ fs.unlinkSync(testDbPath)
19
+ }
20
+
21
+ adapter = setup()
22
+
23
+ // Create test data
24
+ user = await createUser()
25
+ await adapter.create(user.dataObject, undefined)
26
+
27
+ entity = await createEntity()
28
+ await adapter.create(entity.dataObject, undefined)
29
+ })
30
+
31
+ afterAll(async () => {
32
+ try {
33
+ await adapter.deleteCollection('user')
34
+ await adapter.deleteCollection('entities')
35
+ await adapter.close()
36
+
37
+ // Clean up test database file
38
+ if (fs.existsSync(testDbPath)) {
39
+ fs.unlinkSync(testDbPath)
40
+ }
41
+ } catch (err) {
42
+ // Ignore cleanup errors
43
+ }
44
+ })
45
+
46
+ describe('Initialization', () => {
47
+ test('should create SQLiteAdapter instance', () => {
48
+ expect(adapter).toBeInstanceOf(SQLiteAdapter)
49
+ })
50
+
51
+ test('should create adapter with file database', () => {
52
+ const fileAdapter = new SQLiteAdapter({
53
+ config: { database: testDbPath }
54
+ })
55
+ expect(fileAdapter).toBeInstanceOf(SQLiteAdapter)
56
+ })
57
+
58
+ test('should default to in-memory database', () => {
59
+ const defaultAdapter = new SQLiteAdapter()
60
+ expect(defaultAdapter).toBeInstanceOf(SQLiteAdapter)
61
+ })
62
+ })
63
+
64
+ describe('Data Processing Methods', () => {
65
+ test('should handle array to string conversion', () => {
66
+ const testArray = ['value1', 'value2', 'value3']
67
+ const result = (adapter as any)._array2String(testArray)
68
+ expect(result).toBe('\'["value1","value2","value3"]\'')
69
+ })
70
+
71
+ test('should handle empty array conversion', () => {
72
+ const result = (adapter as any)._array2String([])
73
+ expect(result).toBe('\'[]\'')
74
+ })
75
+
76
+ test('should handle mixed type array', () => {
77
+ const result = (adapter as any)._array2String(['string', 123, true])
78
+ expect(result).toBe('\'["string",123,true]\'')
79
+ })
80
+ })
81
+
82
+ describe('Data Preparation', () => {
83
+ test('should filter null values when filterNulls is true', () => {
84
+ const data = {
85
+ name: 'Test',
86
+ description: null,
87
+ value: '',
88
+ count: 42
89
+ }
90
+ const result = (adapter as any)._prepareData(data, true)
91
+ expect(result).toEqual(['Test', 42])
92
+ })
93
+
94
+ test('should include null values when filterNulls is false', () => {
95
+ const data = {
96
+ name: 'Test',
97
+ description: null,
98
+ value: '',
99
+ count: 42
100
+ }
101
+ const result = (adapter as any)._prepareData(data, false)
102
+ expect(result).toEqual(['Test', null, '', 42])
103
+ })
104
+
105
+ test('should handle arrays in data', () => {
106
+ const data = {
107
+ name: 'Test',
108
+ tags: ['tag1', 'tag2']
109
+ }
110
+ const result = (adapter as any)._prepareData(data, false)
111
+ expect(result).toEqual(['Test', '["tag1","tag2"]'])
112
+ })
113
+ })
114
+
115
+ describe('CRUD Operations', () => {
116
+ test('should read a record by path', async () => {
117
+ const readUser = await User.factory()
118
+ readUser.dataObject.uri.path = user.dataObject.uri.path
119
+
120
+ const result = await adapter.read(readUser.dataObject)
121
+ expect(result.val('firstname')).toBe('John')
122
+ expect(result.val('lastname')).toBe('Doe')
123
+ expect(result.val('email')).toBe('john@doe.com')
124
+ })
125
+
126
+ test('should throw NotFoundError for non-existent record', async () => {
127
+ const nonExistentUser = await User.factory()
128
+ nonExistentUser.dataObject.uri.path = 'user/non-existent-id'
129
+
130
+ await expect(adapter.read(nonExistentUser.dataObject)).rejects.toThrow(NotFoundError)
131
+ })
132
+
133
+ test('should update a record', async () => {
134
+ user.dataObject.set('firstname', 'Jane')
135
+ const result = await adapter.update(user.dataObject)
136
+
137
+ expect(result.val('firstname')).toBe('Jane')
138
+
139
+ // Verify in database
140
+ const readUser = await User.factory()
141
+ readUser.dataObject.uri.path = user.dataObject.uri.path
142
+ await adapter.read(readUser.dataObject)
143
+ expect(readUser.dataObject.val('firstname')).toBe('Jane')
144
+ })
145
+
146
+ test('should handle update with no changes', async () => {
147
+ const result = await adapter.update(user.dataObject)
148
+ expect(result).toBe(user.dataObject)
149
+ })
150
+
151
+ test('should throw error when updating object without UID', async () => {
152
+ const newUser = await User.factory()
153
+ await expect(adapter.update(newUser.dataObject)).rejects.toThrow()
154
+ })
155
+
156
+ test('should create a record with desired UID', async () => {
157
+ const testEntity = await createEntity()
158
+ testEntity._.name = 'Entity with Custom ID'
159
+ const customUid = 'custom-test-id-123'
160
+
161
+ const result = await adapter.create(testEntity.dataObject, customUid)
162
+ expect(result.uid).toBe(customUid)
163
+ })
164
+
165
+ test('should throw error when deleting object without UID', async () => {
166
+ const newUser = await User.factory()
167
+ await expect(adapter.delete(newUser.dataObject)).rejects.toThrow(BackendError)
168
+ })
169
+
170
+ test('should soft delete a record', async () => {
171
+ const entityToDelete = await createEntity()
172
+ entityToDelete._.name = 'To Soft Delete'
173
+ await adapter.create(entityToDelete.dataObject, undefined)
174
+
175
+ await adapter.delete(entityToDelete.dataObject, false)
176
+ expect(entityToDelete.dataObject.uri.path).toBe('')
177
+ })
178
+
179
+ test('should hard delete a record', async () => {
180
+ const entityToDelete = await createEntity()
181
+ entityToDelete._.name = 'To Hard Delete'
182
+ await adapter.create(entityToDelete.dataObject, undefined)
183
+
184
+ await adapter.delete(entityToDelete.dataObject, true)
185
+ expect(entityToDelete.dataObject.uri.path).toBe('')
186
+ })
187
+ })
188
+
189
+ describe('Search and Query Operations', () => {
190
+ beforeAll(async () => {
191
+ // Create additional test data for queries
192
+ for (let i = 0; i < 5; i++) {
193
+ const testUser = await User.factory()
194
+ testUser._.firstname = `SearchUser${i}`
195
+ testUser._.lastname = 'SearchTest'
196
+ testUser._.email = `searchuser${i}@test.com`
197
+ testUser._.password = 'password'
198
+ await adapter.create(testUser.dataObject, undefined)
199
+ }
200
+ })
201
+
202
+ test('should find records without filters', async () => {
203
+ const searchUser = await User.factory()
204
+ const result = await adapter.find(searchUser.dataObject)
205
+
206
+ expect(result.items.length).toBeGreaterThan(0)
207
+ expect(result.meta.count).toBeGreaterThan(0)
208
+ })
209
+
210
+ test('should find records with equals filter', async () => {
211
+ const searchUser = await User.factory()
212
+ const filters = [new Filter('firstname', 'SearchUser1', OperatorKeys.equals)]
213
+
214
+ const result = await adapter.find(searchUser.dataObject, filters)
215
+ expect(result.items.length).toBe(1)
216
+ expect(result.items[0].val('firstname')).toBe('SearchUser1')
217
+ })
218
+
219
+ test('should find records with like filter (contains)', async () => {
220
+ const searchUser = await User.factory()
221
+ const filters = [new Filter('lastname', 'SearchTest', OperatorKeys.like)]
222
+
223
+ const result = await adapter.find(searchUser.dataObject, filters)
224
+ expect(result.items.length).toBeGreaterThan(1)
225
+ })
226
+
227
+ test('should find records with keywords filter', async () => {
228
+ const searchUser = await User.factory()
229
+ const filters = [new Filter('keywords', 'SearchTest', OperatorKeys.like)]
230
+
231
+ const result = await adapter.find(searchUser.dataObject, filters)
232
+ expect(result.items.length).toBeGreaterThan(0)
233
+ })
234
+
235
+ test('should handle pagination', async () => {
236
+ const searchUser = await User.factory()
237
+ const pagination = {
238
+ limits: { batch: 2, offset: 0 },
239
+ sortings: [{ prop: 'firstname', order: 'asc' as const }]
240
+ }
241
+
242
+ const result = await adapter.find(searchUser.dataObject, undefined, pagination)
243
+ expect(result.items.length).toBeLessThanOrEqual(2)
244
+ expect(result.meta.batch).toBe(2)
245
+ expect(result.meta.offset).toBe(0)
246
+ })
247
+
248
+ test('should handle sorting', async () => {
249
+ const searchUser = await User.factory()
250
+ const filters = [new Filter('lastname', 'SearchTest', OperatorKeys.equals)]
251
+ const pagination = {
252
+ limits: { batch: 10, offset: 0 },
253
+ sortings: [{ prop: 'firstname', order: 'desc' as const }]
254
+ }
255
+
256
+ const result = await adapter.find(searchUser.dataObject, filters, pagination)
257
+ expect(result.items.length).toBeGreaterThan(1)
258
+
259
+ // Check if sorted in descending order
260
+ for (let i = 1; i < result.items.length; i++) {
261
+ const prev = result.items[i-1].val('firstname')
262
+ const curr = result.items[i].val('firstname')
263
+ expect(prev >= curr).toBe(true)
264
+ }
265
+ })
266
+
267
+ test('should throw error for invalid property filter', async () => {
268
+ const searchUser = await User.factory()
269
+ const filters = [new Filter('nonexistent', 'value', OperatorKeys.equals)]
270
+
271
+ await expect(adapter.find(searchUser.dataObject, filters)).rejects.toThrow(BackendError)
272
+ })
273
+
274
+ test('should handle greater than filter', async () => {
275
+ const searchUser = await User.factory()
276
+ const filters = [new Filter('firstname', 'SearchUser1', OperatorKeys.greater)]
277
+
278
+ const result = await adapter.find(searchUser.dataObject, filters)
279
+ expect(result.items.length).toBeGreaterThan(0)
280
+ })
281
+
282
+ test('should handle not equals filter', async () => {
283
+ const searchUser = await User.factory()
284
+ const filters = [new Filter('firstname', 'SearchUser0', OperatorKeys.notEquals)]
285
+
286
+ const result = await adapter.find(searchUser.dataObject, filters)
287
+ expect(result.items.length).toBeGreaterThan(0)
288
+
289
+ // Verify none of the results have firstname 'SearchUser0'
290
+ result.items.forEach(item => {
291
+ expect(item.val('firstname')).not.toBe('SearchUser0')
292
+ })
293
+ })
294
+ })
295
+
296
+ describe('Table Management', () => {
297
+ test('should create table automatically for new collections', async () => {
298
+ // Create a new type of object that doesn't exist yet
299
+ const newUser = await User.factory()
300
+ newUser._.firstname = 'Auto'
301
+ newUser._.lastname = 'Table'
302
+ newUser._.email = 'auto@table.com'
303
+ newUser._.password = 'password'
304
+
305
+ // This should automatically create the table
306
+ const result = await adapter.create(newUser.dataObject, undefined)
307
+ expect(result.uid).toBeDefined()
308
+ })
309
+
310
+ test('should handle table existence check', async () => {
311
+ // Test _ensureTable method indirectly through operations
312
+ const user = await User.factory()
313
+ user._.firstname = 'Table'
314
+ user._.lastname = 'Exists'
315
+ user._.email = 'table@exists.com'
316
+ user._.password = 'password'
317
+
318
+ await adapter.create(user.dataObject, undefined)
319
+
320
+ // Should work fine with existing table
321
+ const result = await adapter.find(user.dataObject)
322
+ expect(result.items.length).toBeGreaterThan(0)
323
+ })
324
+ })
325
+
326
+ describe('Error Handling', () => {
327
+ test('should handle invalid path format', async () => {
328
+ const testUser = await User.factory()
329
+ testUser.dataObject.uri.path = 'invalid/path/format/too/many/parts'
330
+
331
+ await expect(adapter.read(testUser.dataObject)).rejects.toThrow(NotFoundError)
332
+ })
333
+
334
+ test('should handle missing collection', async () => {
335
+ const testUser = await User.factory()
336
+ // Force collection to be undefined
337
+ Object.defineProperty(testUser.dataObject.uri, 'collection', {
338
+ value: undefined,
339
+ writable: true
340
+ })
341
+
342
+ await expect(adapter.create(testUser.dataObject, undefined)).rejects.toThrow(BackendError)
343
+ })
344
+
345
+ test('should handle creation of object that already has UID', async () => {
346
+ const testUser = await User.factory()
347
+ testUser.dataObject.uri.path = 'user/existing-id'
348
+
349
+ await expect(adapter.create(testUser.dataObject, undefined)).rejects.toThrow(BackendError)
350
+ })
351
+ })
352
+
353
+ describe('Collection Operations', () => {
354
+ test('should delete entire collection', async () => {
355
+ // Create a test entity
356
+ const testEntity = await createEntity()
357
+ testEntity._.name = 'Collection Test'
358
+ await adapter.create(testEntity.dataObject, undefined)
359
+
360
+ // Delete collection
361
+ await adapter.deleteCollection('entities')
362
+
363
+ // Verify collection is empty by trying to find records
364
+ const result = await adapter.find(testEntity.dataObject)
365
+ expect(result.items.length).toBe(0)
366
+ })
367
+
368
+ test('should handle deleting non-existent collection', async () => {
369
+ // Should not throw error when deleting non-existent collection
370
+ await expect(adapter.deleteCollection('nonexistent')).resolves.not.toThrow()
371
+ })
372
+ })
373
+
374
+ describe('Complex Data Types', () => {
375
+ test('should handle array properties', async () => {
376
+ // This would require a model with array properties
377
+ // For now, we'll test the data preparation methods
378
+ const data = {
379
+ tags: ['web', 'mobile', 'api'],
380
+ categories: ['tech', 'business']
381
+ }
382
+
383
+ const prepared = (adapter as any)._prepareData(data, false)
384
+ expect(prepared).toContain('["web","mobile","api"]')
385
+ expect(prepared).toContain('["tech","business"]')
386
+ })
387
+
388
+ test('should handle object references', async () => {
389
+ const testUser = await User.factory()
390
+ testUser._.firstname = 'Ref'
391
+ testUser._.lastname = 'Test'
392
+ testUser._.email = 'ref@test.com'
393
+ testUser._.password = 'password'
394
+ testUser._.entity = entity
395
+
396
+ const result = await adapter.create(testUser.dataObject, undefined)
397
+ expect(result.uid).toBeDefined()
398
+
399
+ // Read back and verify reference
400
+ const readUser = await User.factory()
401
+ readUser.dataObject.uri.path = result.uri.path
402
+ const readResult = await adapter.read(readUser.dataObject)
403
+
404
+ // The entity reference should be properly handled
405
+ expect(readResult.val('entity')).toBeDefined()
406
+ })
407
+ })
408
+
409
+ describe('Connection Management', () => {
410
+ test('should close connection properly', async () => {
411
+ const tempAdapter = setup()
412
+
413
+ // Create a connection by doing an operation
414
+ const user = await User.factory()
415
+ await tempAdapter.create(user.dataObject, undefined)
416
+
417
+ // Close connection
418
+ await tempAdapter.close()
419
+
420
+ // Should be able to close without error
421
+ expect(true).toBe(true)
422
+ })
423
+
424
+ test('should handle multiple close calls', async () => {
425
+ const tempAdapter = setup()
426
+
427
+ await tempAdapter.close()
428
+ await tempAdapter.close() // Should not throw
429
+
430
+ expect(true).toBe(true)
431
+ })
432
+ })
433
+ })