@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,325 @@
1
+ import { User } from '@quatrain/backend'
2
+ import { Filter, OperatorKeys } from '@quatrain/backend'
3
+ import { setup, createUser, createEntity, Entity } from './common'
4
+
5
+ describe('SQLite Search Operations', () => {
6
+ let adapter: any
7
+ let user: User
8
+ let entity: Entity
9
+
10
+ beforeAll(async () => {
11
+ adapter = setup()
12
+
13
+ // Create test entity
14
+ entity = await createEntity()
15
+ await adapter.create(entity.dataObject, undefined)
16
+
17
+ // Create multiple test users
18
+ const users = []
19
+ for (let i = 0; i < 10; i++) {
20
+ const testUser = await User.factory()
21
+ testUser._.firstname = `TestUser${i}`
22
+ testUser._.lastname = i % 2 === 0 ? 'Even' : 'Odd'
23
+ testUser._.email = `testuser${i}@example.com`
24
+ testUser._.password = 'password'
25
+
26
+ // Some users belong to the entity
27
+ if (i < 5) {
28
+ testUser._.entity = entity
29
+ }
30
+
31
+ await adapter.create(testUser.dataObject, undefined)
32
+ users.push(testUser)
33
+ }
34
+
35
+ user = users[0]
36
+ })
37
+
38
+ afterAll(async () => {
39
+ await adapter.deleteCollection('user')
40
+ await adapter.deleteCollection('entities')
41
+ await adapter.close()
42
+ })
43
+
44
+ describe('Basic Search Operations', () => {
45
+ test('find all users without filters', async () => {
46
+ const searchUser = await User.factory()
47
+ const result = await adapter.find(searchUser.dataObject)
48
+
49
+ expect(result.items.length).toBe(10)
50
+ expect(result.meta.count).toBe(10)
51
+ expect(result.meta.batch).toBe(20) // Default batch size
52
+ expect(result.meta.offset).toBe(0)
53
+ })
54
+
55
+ test('find users with firstname filter', async () => {
56
+ const searchUser = await User.factory()
57
+ const filters = [new Filter('firstname', 'TestUser5', OperatorKeys.equals)]
58
+
59
+ const result = await adapter.find(searchUser.dataObject, filters)
60
+ expect(result.items.length).toBe(1)
61
+ expect(result.items[0].val('firstname')).toBe('TestUser5')
62
+ })
63
+
64
+ test('find users with lastname filter', async () => {
65
+ const searchUser = await User.factory()
66
+ const filters = [new Filter('lastname', 'Even', OperatorKeys.equals)]
67
+
68
+ const result = await adapter.find(searchUser.dataObject, filters)
69
+ expect(result.items.length).toBe(5)
70
+
71
+ result.items.forEach((item: any) => {
72
+ expect(item.val('lastname')).toBe('Even')
73
+ })
74
+ })
75
+
76
+ test('find users with multiple filters', async () => {
77
+ const searchUser = await User.factory()
78
+ const filters = [
79
+ new Filter('lastname', 'Even', OperatorKeys.equals),
80
+ new Filter('firstname', 'TestUser0', OperatorKeys.equals)
81
+ ]
82
+
83
+ const result = await adapter.find(searchUser.dataObject, filters)
84
+ expect(result.items.length).toBe(1)
85
+ expect(result.items[0].val('firstname')).toBe('TestUser0')
86
+ expect(result.items[0].val('lastname')).toBe('Even')
87
+ })
88
+ })
89
+
90
+ describe('Comparison Operators', () => {
91
+ test('greater than filter', async () => {
92
+ const searchUser = await User.factory()
93
+ const filters = [new Filter('firstname', 'TestUser5', OperatorKeys.greater)]
94
+
95
+ const result = await adapter.find(searchUser.dataObject, filters)
96
+ expect(result.items.length).toBeGreaterThan(0)
97
+
98
+ result.items.forEach((item: any) => {
99
+ expect(item.val('firstname') > 'TestUser5').toBe(true)
100
+ })
101
+ })
102
+
103
+ test('less than filter', async () => {
104
+ const searchUser = await User.factory()
105
+ const filters = [new Filter('firstname', 'TestUser5', OperatorKeys.lower)]
106
+
107
+ const result = await adapter.find(searchUser.dataObject, filters)
108
+ expect(result.items.length).toBeGreaterThan(0)
109
+
110
+ result.items.forEach((item: any) => {
111
+ expect(item.val('firstname') < 'TestUser5').toBe(true)
112
+ })
113
+ })
114
+
115
+ test('not equals filter', async () => {
116
+ const searchUser = await User.factory()
117
+ const filters = [new Filter('lastname', 'Even', OperatorKeys.notEquals)]
118
+
119
+ const result = await adapter.find(searchUser.dataObject, filters)
120
+ expect(result.items.length).toBe(5)
121
+
122
+ result.items.forEach((item: any) => {
123
+ expect(item.val('lastname')).not.toBe('Even')
124
+ })
125
+ })
126
+
127
+ test('greater than or equals filter', async () => {
128
+ const searchUser = await User.factory()
129
+ const filters = [new Filter('firstname', 'TestUser5', OperatorKeys.greaterOrEquals)]
130
+
131
+ const result = await adapter.find(searchUser.dataObject, filters)
132
+ expect(result.items.length).toBeGreaterThan(0)
133
+
134
+ result.items.forEach((item: any) => {
135
+ expect(item.val('firstname') >= 'TestUser5').toBe(true)
136
+ })
137
+ })
138
+
139
+ test('less than or equals filter', async () => {
140
+ const searchUser = await User.factory()
141
+ const filters = [new Filter('firstname', 'TestUser5', OperatorKeys.lowerOrEquals)]
142
+
143
+ const result = await adapter.find(searchUser.dataObject, filters)
144
+ expect(result.items.length).toBeGreaterThan(0)
145
+
146
+ result.items.forEach((item: any) => {
147
+ expect(item.val('firstname') <= 'TestUser5').toBe(true)
148
+ })
149
+ })
150
+ })
151
+
152
+ describe('Text Search Operations', () => {
153
+ test('keywords search across multiple fields', async () => {
154
+ const searchUser = await User.factory()
155
+ const filters = [new Filter('keywords', 'TestUser', OperatorKeys.like)]
156
+
157
+ const result = await adapter.find(searchUser.dataObject, filters)
158
+ expect(result.items.length).toBe(10) // All users have "TestUser" in firstname
159
+ })
160
+
161
+ test('keywords search with specific term', async () => {
162
+ const searchUser = await User.factory()
163
+ const filters = [new Filter('keywords', 'testuser3@example.com', OperatorKeys.like)]
164
+
165
+ const result = await adapter.find(searchUser.dataObject, filters)
166
+ expect(result.items.length).toBeGreaterThan(0)
167
+ })
168
+
169
+ test('like filter on specific field', async () => {
170
+ const searchUser = await User.factory()
171
+ const filters = [new Filter('email', '@example.com', OperatorKeys.like)]
172
+
173
+ const result = await adapter.find(searchUser.dataObject, filters)
174
+ expect(result.items.length).toBe(10) // All test users have @example.com
175
+ })
176
+ })
177
+
178
+ describe('Pagination and Sorting', () => {
179
+ test('pagination with limit', async () => {
180
+ const searchUser = await User.factory()
181
+ const pagination = {
182
+ limits: { batch: 3, offset: 0 },
183
+ sortings: [{ prop: 'firstname', order: 'asc' as const }]
184
+ }
185
+
186
+ const result = await adapter.find(searchUser.dataObject, undefined, pagination)
187
+ expect(result.items.length).toBe(3)
188
+ expect(result.meta.batch).toBe(3)
189
+ expect(result.meta.offset).toBe(0)
190
+ expect(result.meta.count).toBe(10) // Total count should still be 10
191
+ })
192
+
193
+ test('pagination with offset', async () => {
194
+ const searchUser = await User.factory()
195
+ const pagination = {
196
+ limits: { batch: 3, offset: 3 },
197
+ sortings: [{ prop: 'firstname', order: 'asc' as const }]
198
+ }
199
+
200
+ const result = await adapter.find(searchUser.dataObject, undefined, pagination)
201
+ expect(result.items.length).toBe(3)
202
+ expect(result.meta.offset).toBe(3)
203
+ })
204
+
205
+ test('sorting ascending', async () => {
206
+ const searchUser = await User.factory()
207
+ const pagination = {
208
+ limits: { batch: 5, offset: 0 },
209
+ sortings: [{ prop: 'firstname', order: 'asc' as const }]
210
+ }
211
+
212
+ const result = await adapter.find(searchUser.dataObject, undefined, pagination)
213
+
214
+ for (let i = 1; i < result.items.length; i++) {
215
+ const prev = result.items[i-1].val('firstname')
216
+ const curr = result.items[i].val('firstname')
217
+ expect(prev <= curr).toBe(true)
218
+ }
219
+ })
220
+
221
+ test('sorting descending', async () => {
222
+ const searchUser = await User.factory()
223
+ const pagination = {
224
+ limits: { batch: 5, offset: 0 },
225
+ sortings: [{ prop: 'firstname', order: 'desc' as const }]
226
+ }
227
+
228
+ const result = await adapter.find(searchUser.dataObject, undefined, pagination)
229
+
230
+ for (let i = 1; i < result.items.length; i++) {
231
+ const prev = result.items[i-1].val('firstname')
232
+ const curr = result.items[i].val('firstname')
233
+ expect(prev >= curr).toBe(true)
234
+ }
235
+ })
236
+
237
+ test('multiple sorting criteria', async () => {
238
+ const searchUser = await User.factory()
239
+ const pagination = {
240
+ limits: { batch: 10, offset: 0 },
241
+ sortings: [
242
+ { prop: 'lastname', order: 'asc' as const },
243
+ { prop: 'firstname', order: 'desc' as const }
244
+ ]
245
+ }
246
+
247
+ const result = await adapter.find(searchUser.dataObject, undefined, pagination)
248
+ expect(result.items.length).toBe(10)
249
+ expect(result.meta.sortField).toContain('lastname ASC')
250
+ expect(result.meta.sortField).toContain('firstname DESC')
251
+ })
252
+ })
253
+
254
+ describe('Object Reference Filters', () => {
255
+ test('filter by entity reference', async () => {
256
+ const searchUser = await User.factory()
257
+ const filters = [new Filter('entity', entity, OperatorKeys.equals)]
258
+
259
+ const result = await adapter.find(searchUser.dataObject, filters)
260
+ expect(result.items.length).toBe(5) // 5 users have this entity
261
+
262
+ result.items.forEach((item: any) => {
263
+ const entityValue = item.val('entity')
264
+ expect(entityValue).toBeDefined()
265
+ })
266
+ })
267
+
268
+ test('filter by null entity', async () => {
269
+ const searchUser = await User.factory()
270
+ const filters = [new Filter('entity', 'null', OperatorKeys.equals)]
271
+
272
+ const result = await adapter.find(searchUser.dataObject, filters)
273
+ expect(result.items.length).toBe(5) // 5 users don't have entity
274
+ })
275
+ })
276
+
277
+ describe('Complex Queries', () => {
278
+ test('combined filters with pagination and sorting', async () => {
279
+ const searchUser = await User.factory()
280
+ const filters = [new Filter('lastname', 'Even', OperatorKeys.equals)]
281
+ const pagination = {
282
+ limits: { batch: 2, offset: 1 },
283
+ sortings: [{ prop: 'firstname', order: 'asc' as const }]
284
+ }
285
+
286
+ const result = await adapter.find(searchUser.dataObject, filters, pagination)
287
+ expect(result.items.length).toBe(2)
288
+ expect(result.meta.count).toBe(5) // Total matching records
289
+ expect(result.meta.offset).toBe(1)
290
+
291
+ result.items.forEach((item: any) => {
292
+ expect(item.val('lastname')).toBe('Even')
293
+ })
294
+ })
295
+
296
+ test('no results query', async () => {
297
+ const searchUser = await User.factory()
298
+ const filters = [new Filter('firstname', 'NonExistentUser', OperatorKeys.equals)]
299
+
300
+ const result = await adapter.find(searchUser.dataObject, filters)
301
+ expect(result.items.length).toBe(0)
302
+ expect(result.meta.count).toBe(0)
303
+ })
304
+ })
305
+
306
+ describe('Error Cases', () => {
307
+ test('invalid property name in filter', async () => {
308
+ const searchUser = await User.factory()
309
+ const filters = [new Filter('invalidProperty', 'value', OperatorKeys.equals)]
310
+
311
+ await expect(adapter.find(searchUser.dataObject, filters)).rejects.toThrow()
312
+ })
313
+
314
+ test('invalid collection for search', async () => {
315
+ const searchUser = await User.factory()
316
+ // Force collection to be undefined
317
+ Object.defineProperty(searchUser.dataObject.uri, 'collection', {
318
+ value: undefined,
319
+ writable: true
320
+ })
321
+
322
+ await expect(adapter.find(searchUser.dataObject)).rejects.toThrow()
323
+ })
324
+ })
325
+ })
@@ -0,0 +1,61 @@
1
+ import { User, PersistedBaseObject } from '@quatrain/backend'
2
+ import { Entity as CoreEntity } from '@quatrain/core'
3
+ import { SQLiteAdapter } from '../SQLiteAdapter'
4
+
5
+ // Create a simple Entity class that extends PersistedBaseObject for testing
6
+ class Entity extends PersistedBaseObject {
7
+ static COLLECTION = 'entities'
8
+ static PROPS_DEFINITION = CoreEntity.PROPS_DEFINITION
9
+
10
+ static async factory(src: any = undefined): Promise<Entity> {
11
+ return super.factory(src, Entity)
12
+ }
13
+ }
14
+
15
+ export const setup = (dbPath: string = ':memory:') => {
16
+ return new SQLiteAdapter({
17
+ config: {
18
+ database: dbPath
19
+ },
20
+ })
21
+ }
22
+
23
+ export const createUser = async () => {
24
+ const user = await User.factory()
25
+ user._.firstname = 'John'
26
+ user._.lastname = 'Doe'
27
+ user._.email = 'john@doe.com'
28
+ user._.password = 'password123'
29
+
30
+ return user
31
+ }
32
+
33
+ export const createEntity = async () => {
34
+ const entity = await Entity.factory()
35
+ entity._.name = 'ACME Inc.'
36
+
37
+ return entity
38
+ }
39
+
40
+ export const createUsers = async (
41
+ userModel: User,
42
+ qty: number = 5,
43
+ forcedValues: any = {}
44
+ ): Promise<User[]> => {
45
+ // Simple data generator since utils.DataGenerator is not available
46
+ const users: User[] = []
47
+ for (let i = 0; i < qty; i++) {
48
+ const user = await User.factory()
49
+ user._.firstname = forcedValues.firstname || `User${i}`
50
+ user._.lastname = forcedValues.lastname || 'Test'
51
+ user._.email = forcedValues.email || `user${i}@test.com`
52
+ user._.password = forcedValues.password || 'password'
53
+ if (forcedValues.entity) {
54
+ user._.entity = forcedValues.entity
55
+ }
56
+ users.push(user)
57
+ }
58
+ return users
59
+ }
60
+
61
+ export { Entity }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { SQLiteAdapter } from './SQLiteAdapter'
2
+
3
+ export { SQLiteAdapter }
@@ -1,55 +0,0 @@
1
- import { DataObjectClass, AbstractBackendAdapter, BackendParameters, QueryResultType, Filters, Filter, SortAndLimit } from '@quatrain/backend';
2
- import sqlite3 from 'sqlite3';
3
- import { Database } from 'sqlite';
4
- /**
5
- * SQLite Backend Adapter for Quatrain
6
- */
7
- export declare class SQLiteAdapter extends AbstractBackendAdapter {
8
- protected _connection: undefined | Database<sqlite3.Database>;
9
- protected _dbPath: string;
10
- constructor(params?: BackendParameters);
11
- protected _buildPath(dataObject: DataObjectClass<any>, uid?: string): string;
12
- protected _connect(): Promise<Database<sqlite3.Database>>;
13
- /**
14
- * Process data for compatibility
15
- * @param data
16
- * @param filterNulls
17
- * @returns
18
- */
19
- protected _prepareData(data: any, filterNulls?: boolean): any;
20
- /**
21
- * Ensure the collection table exists in SQLite
22
- * @param dataObject DataObject to create table for
23
- */
24
- private _ensureTable;
25
- /**
26
- * Create record in backend
27
- * @param dataObject DataObject instance to persist in backend
28
- * @param desiredUid Desired unique ID for record
29
- * @returns DataObject
30
- */
31
- create(dataObject: DataObjectClass<any>, desiredUid: string | undefined): Promise<DataObjectClass<any>>;
32
- read(dataObject: DataObjectClass<any>): Promise<DataObjectClass<any>>;
33
- update(dataObject: DataObjectClass<any>): Promise<DataObjectClass<any>>;
34
- delete(dataObject: DataObjectClass<any>, hardDelete?: boolean): Promise<DataObjectClass<any>>;
35
- deleteCollection(collection: string, batchSize?: number): Promise<void>;
36
- /**
37
- * Convert array into SQL expression
38
- * @param from Array of strings or numbers
39
- * @returns string
40
- */
41
- protected _array2String(from: (string | number)[]): string;
42
- /**
43
- * Execute a query on a collection
44
- * @param dataObject
45
- * @param filters
46
- * @param pagination
47
- * @params parent
48
- * @returns
49
- */
50
- find(dataObject: DataObjectClass<any>, filters?: Filters | Filter[] | undefined, pagination?: SortAndLimit | undefined, parent?: DataObjectClass<any> | undefined): Promise<QueryResultType<DataObjectClass<any>>>;
51
- /**
52
- * Close the SQLite connection
53
- */
54
- close(): Promise<void>;
55
- }