cozy-pouch-link 57.5.0 → 57.6.1

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 (45) hide show
  1. package/dist/CozyPouchLink.js +234 -470
  2. package/dist/CozyPouchLink.spec.js +6 -147
  3. package/dist/PouchManager.js +43 -8
  4. package/dist/PouchManager.spec.js +21 -12
  5. package/dist/__mocks__/@op-engineering/op-sqlite.js +11 -0
  6. package/dist/db/dbInterface.js +190 -0
  7. package/dist/db/helpers.js +128 -0
  8. package/dist/db/pouchdb/getDocs.js +157 -0
  9. package/dist/db/pouchdb/getDocs.spec.js +63 -0
  10. package/dist/db/pouchdb/pouchdb.js +264 -0
  11. package/dist/db/pouchdb/pouchdb.spec.js +151 -0
  12. package/dist/db/sqlite/sql.js +422 -0
  13. package/dist/db/sqlite/sql.spec.js +419 -0
  14. package/dist/db/sqlite/sqliteDb.js +41 -0
  15. package/dist/db/sqlite/sqliteDb.native.js +317 -0
  16. package/dist/errors.js +17 -2
  17. package/dist/helpers.js +21 -147
  18. package/dist/helpers.spec.js +1 -98
  19. package/dist/index.js +9 -1
  20. package/dist/jsonapi.js +57 -21
  21. package/dist/jsonapi.spec.js +105 -32
  22. package/dist/mango.js +146 -3
  23. package/dist/migrations/pouchdb.js +32 -0
  24. package/dist/replicateOnce.js +25 -23
  25. package/dist/types.js +5 -0
  26. package/dist/utils.js +33 -3
  27. package/package.json +5 -3
  28. package/types/CozyPouchLink.d.ts +6 -63
  29. package/types/PouchManager.d.ts +6 -1
  30. package/types/__mocks__/@op-engineering/op-sqlite.d.ts +1 -0
  31. package/types/db/dbInterface.d.ts +117 -0
  32. package/types/db/helpers.d.ts +4 -0
  33. package/types/db/pouchdb/getDocs.d.ts +18 -0
  34. package/types/db/pouchdb/pouchdb.d.ts +8 -0
  35. package/types/db/sqlite/sql.d.ts +45 -0
  36. package/types/db/sqlite/sqliteDb.d.ts +4 -0
  37. package/types/db/sqlite/sqliteDb.native.d.ts +7 -0
  38. package/types/errors.d.ts +2 -0
  39. package/types/helpers.d.ts +1 -4
  40. package/types/index.d.ts +1 -0
  41. package/types/jsonapi.d.ts +4 -2
  42. package/types/mango.d.ts +19 -1
  43. package/types/migrations/pouchdb.d.ts +1 -0
  44. package/types/types.d.ts +2 -0
  45. package/types/utils.d.ts +3 -0
@@ -0,0 +1,419 @@
1
+ import {
2
+ mangoSelectorToSQL,
3
+ makeWhereClause,
4
+ makeSortClause,
5
+ makeSQLQueryFromMango,
6
+ keepDocWitHighestRev,
7
+ makeSQLQueryAll,
8
+ parseResults
9
+ } from './sql'
10
+
11
+ describe('mangoSelectorToSQL', () => {
12
+ it('should return empty string for empty selector', () => {
13
+ const selector = {}
14
+ expect(mangoSelectorToSQL(selector)).toBe('')
15
+ })
16
+
17
+ it('should handle implicit equality selector', () => {
18
+ const selector = { status: 'active' }
19
+ expect(mangoSelectorToSQL(selector)).toBe(
20
+ "json_extract(data, '$.status') = 'active'"
21
+ )
22
+ })
23
+
24
+ it('should handle explicit $eq operator', () => {
25
+ const selector = { status: { $eq: 'active' } }
26
+ expect(mangoSelectorToSQL(selector)).toBe(
27
+ "json_extract(data, '$.status') = 'active'"
28
+ )
29
+ })
30
+
31
+ it('should handle $neq operator', () => {
32
+ const selector = { status: { $ne: 'active' } }
33
+ expect(mangoSelectorToSQL(selector)).toBe(
34
+ "json_extract(data, '$.status') != 'active'"
35
+ )
36
+ })
37
+
38
+ it('should handle $in and $nin operator', () => {
39
+ const selector = {
40
+ status: { $in: ['active', 'pending'] },
41
+ other_status: { $nin: ['maintenance', 'failing'] }
42
+ }
43
+ expect(mangoSelectorToSQL(selector)).toBe(
44
+ "json_extract(data, '$.status') IN ('active', 'pending') AND json_extract(data, '$.other_status') NOT IN ('maintenance', 'failing')"
45
+ )
46
+ })
47
+
48
+ it('should handle $exists operator', () => {
49
+ const selector1 = { status: { $exists: true } }
50
+ expect(mangoSelectorToSQL(selector1)).toBe(
51
+ "json_extract(data, '$.status') IS NOT NULL"
52
+ )
53
+
54
+ const selector2 = { status: { $exists: false } }
55
+ expect(mangoSelectorToSQL(selector2)).toBe(
56
+ "json_extract(data, '$.status') IS NULL"
57
+ )
58
+ })
59
+
60
+ it('should handle implicit $and operator', () => {
61
+ const selector = {
62
+ age: 18,
63
+ status: 'active',
64
+ date: '2025-01-01'
65
+ }
66
+ expect(mangoSelectorToSQL(selector)).toBe(
67
+ "json_extract(data, '$.age') = 18 AND json_extract(data, '$.status') = 'active' AND json_extract(data, '$.date') = '2025-01-01'"
68
+ )
69
+ })
70
+
71
+ it('should handle range operators', () => {
72
+ const selector1 = { date: { $gt: '2025-01-01', $lt: '2026-01-01' } }
73
+ expect(mangoSelectorToSQL(selector1)).toBe(
74
+ "json_extract(data, '$.date') > '2025-01-01' AND json_extract(data, '$.date') < '2026-01-01'"
75
+ )
76
+
77
+ const selector2 = {
78
+ startDate: { $gte: '2025-01-01' },
79
+ endDate: { $lte: '2026-01-01' }
80
+ }
81
+ expect(mangoSelectorToSQL(selector2)).toBe(
82
+ "json_extract(data, '$.startDate') >= '2025-01-01' AND json_extract(data, '$.endDate') <= '2026-01-01'"
83
+ )
84
+ })
85
+
86
+ it('should handle $gt: null cases', () => {
87
+ const selector = { date: { $gt: null } }
88
+ expect(mangoSelectorToSQL(selector)).toBe(
89
+ "json_extract(data, '$.date') IS NOT NULL"
90
+ )
91
+ })
92
+
93
+ it('should handle explicit $and operator', () => {
94
+ const selector = { $and: [{ age: { $gte: 18 } }, { status: 'active' }] }
95
+ expect(mangoSelectorToSQL(selector)).toBe(
96
+ "(json_extract(data, '$.age') >= 18) AND (json_extract(data, '$.status') = 'active')"
97
+ )
98
+ })
99
+
100
+ it('should handle explicit $or operator', () => {
101
+ const selector = { $or: [{ status: 'active' }, { status: 'pending' }] }
102
+ expect(mangoSelectorToSQL(selector)).toBe(
103
+ "(json_extract(data, '$.status') = 'active') OR (json_extract(data, '$.status') = 'pending')"
104
+ )
105
+ })
106
+ })
107
+
108
+ describe('makeWhereClause', () => {
109
+ it('should return only deleted clause when no mango selector', () => {
110
+ expect(makeWhereClause(undefined)).toEqual('DELETED = 0')
111
+ })
112
+
113
+ it('should return deleted and mango clauses when there is a mango selector', () => {
114
+ const selector = { status: 'active' }
115
+ expect(makeWhereClause(selector)).toEqual(
116
+ "DELETED = 0 AND json_extract(data, '$.status') = 'active'"
117
+ )
118
+ })
119
+ })
120
+
121
+ describe('makeSortClause', () => {
122
+ it('should return null when no mango sort', () => {
123
+ const sortBy = undefined
124
+ expect(makeSortClause(sortBy)).toBe(null)
125
+ })
126
+
127
+ it('should return correct order by, with one sorting attribute', () => {
128
+ const sortBy = [{ date: 'asc' }]
129
+ expect(makeSortClause(sortBy)).toEqual("json_extract(data, '$.date') ASC")
130
+ })
131
+
132
+ it('should return correct order by, with multiple sorting attribute', () => {
133
+ const sortBy = [{ date: 'asc' }, { name: 'asc' }, { type: 'asc' }]
134
+ expect(makeSortClause(sortBy)).toEqual(
135
+ "json_extract(data, '$.date'), json_extract(data, '$.name'), json_extract(data, '$.type') ASC"
136
+ )
137
+ })
138
+
139
+ it('should deal with ascending and descending order', () => {
140
+ const sortBy1 = [{ date: 'asc' }]
141
+ expect(makeSortClause(sortBy1)).toEqual("json_extract(data, '$.date') ASC")
142
+ const sortBy2 = [{ date: 'desc' }]
143
+ expect(makeSortClause(sortBy2)).toEqual("json_extract(data, '$.date') DESC")
144
+ })
145
+ })
146
+
147
+ describe('makeSQLQueryFromMango', () => {
148
+ it('should return a correct SQL query with no sort', () => {
149
+ const selector = { date: { $gt: '2025-01-01' } }
150
+ const indexName = 'by_name'
151
+ const limit = 100
152
+ const sql = makeSQLQueryFromMango({ selector, indexName, limit })
153
+
154
+ const expectedSql = [
155
+ `SELECT json AS data, doc_id, rev`,
156
+ `FROM 'by-sequence' INDEXED BY by_name`,
157
+ `WHERE DELETED = 0 AND json_extract(data, '$.date') > '2025-01-01'`,
158
+ `LIMIT 100;`
159
+ ].join(' ')
160
+ expect(sql).toEqual(expectedSql)
161
+ })
162
+
163
+ it('should return a correct SQL query with sort', () => {
164
+ const selector = { date: { $gt: '2025-01-01' } }
165
+ const sort = [{ date: 'asc' }]
166
+ const indexName = 'by_name'
167
+ const limit = 100
168
+ const sql = makeSQLQueryFromMango({ selector, sort, indexName, limit })
169
+
170
+ const expectedSql = [
171
+ `SELECT json AS data, doc_id, rev`,
172
+ `FROM 'by-sequence' INDEXED BY by_name`,
173
+ `WHERE DELETED = 0 AND json_extract(data, '$.date') > '2025-01-01'`,
174
+ `ORDER BY json_extract(data, '$.date') ASC`,
175
+ `LIMIT 100;`
176
+ ].join(' ')
177
+ expect(sql).toEqual(expectedSql)
178
+ })
179
+
180
+ it('should handle the skip and limit', () => {
181
+ const selector = { date: { $gt: '2025-01-01' } }
182
+ const indexName = 'by_name'
183
+ const limit = 200
184
+ const skip = 100
185
+ const sql = makeSQLQueryFromMango({
186
+ selector,
187
+ indexName,
188
+ limit,
189
+ skip
190
+ })
191
+
192
+ const expectedSql = [
193
+ `SELECT json AS data, doc_id, rev`,
194
+ `FROM 'by-sequence' INDEXED BY by_name`,
195
+ `WHERE DELETED = 0 AND json_extract(data, '$.date') > '2025-01-01'`,
196
+ `OFFSET 100`,
197
+ `LIMIT 200;`
198
+ ].join(' ')
199
+ expect(sql).toEqual(expectedSql)
200
+ })
201
+ })
202
+
203
+ describe('makeSQLQueryAll', () => {
204
+ it('should return a correct sql query to get all docs', () => {
205
+ const sql = makeSQLQueryAll()
206
+ const expectedSql = [
207
+ `SELECT json AS data, doc_id, rev`,
208
+ `FROM 'by-sequence'`,
209
+ `WHERE deleted=0`,
210
+ `LIMIT -1;`
211
+ ].join(' ')
212
+ expect(sql).toEqual(expectedSql)
213
+ })
214
+
215
+ it('should handle limit and skip', () => {
216
+ const sql = makeSQLQueryAll({ limit: 10, skip: 100 })
217
+ const expectedSql = [
218
+ `SELECT json AS data, doc_id, rev`,
219
+ `FROM 'by-sequence'`,
220
+ `WHERE deleted=0`,
221
+ `LIMIT 10`,
222
+ `OFFSET 100;`
223
+ ].join(' ')
224
+ expect(sql).toEqual(expectedSql)
225
+ })
226
+ })
227
+
228
+ describe('keepDocWitHighestRev', () => {
229
+ it('should return null if no docs', () => {
230
+ expect(keepDocWitHighestRev([])).toBeNull()
231
+ expect(keepDocWitHighestRev(undefined)).toBeNull()
232
+ })
233
+
234
+ it('should return the single document when only one is provided', () => {
235
+ const doc = { _rev: '1-a', name: 'Single Doc' }
236
+ const docs = [doc]
237
+ expect(keepDocWitHighestRev(docs)).toBe(doc)
238
+ })
239
+
240
+ it('should return the document with the highest revision prefix', () => {
241
+ const docs = [
242
+ { _rev: '1-a', name: 'Doc 1' },
243
+ { _rev: '3-c', name: 'Doc 3' },
244
+ { _rev: '2-b', name: 'Doc 2' }
245
+ ]
246
+ expect(keepDocWitHighestRev(docs)).toEqual(docs[1])
247
+ })
248
+
249
+ it('should work correctly even if the documents are unsorted', () => {
250
+ const docs = [
251
+ { _rev: '5-zzz', name: 'Doc 5' },
252
+ { _rev: '2-aaa', name: 'Doc 2' },
253
+ { _rev: '10-xxx', name: 'Doc 10' },
254
+ { _rev: '7-bbb', name: 'Doc 7' }
255
+ ]
256
+ expect(keepDocWitHighestRev(docs)).toEqual(docs[2])
257
+ })
258
+ })
259
+
260
+ describe('parseResults', () => {
261
+ const client = {}
262
+ const doctype = 'testdoctype'
263
+
264
+ it('should parse results correctly for multiple documents', () => {
265
+ const result = {
266
+ rows: {
267
+ length: 2,
268
+ item: jest.fn().mockImplementation(i => ({
269
+ data: JSON.stringify({ name: `doc${i}` }),
270
+ doc_id: `id${i}`,
271
+ rev: `rev${i}`
272
+ }))
273
+ }
274
+ }
275
+
276
+ const parsed = parseResults(client, result, doctype)
277
+
278
+ expect(parsed.data.length).toBe(2)
279
+ expect(parsed.meta.count).toBe(2)
280
+ expect(parsed.skip).toBe(0)
281
+ expect(parsed.next).toBe(false)
282
+
283
+ expect(parsed.data[0]).toEqual({
284
+ _id: 'id0',
285
+ id: 'id0',
286
+ _rev: 'rev0',
287
+ _type: doctype,
288
+ name: 'doc0'
289
+ })
290
+ })
291
+
292
+ it('should handle isSingleDoc correctly with multiple docs', () => {
293
+ const result = {
294
+ rows: {
295
+ length: 2,
296
+ item: jest.fn().mockImplementation(i => ({
297
+ data: JSON.stringify({ name: `doc${i}` }),
298
+ doc_id: `id${i}`,
299
+ rev: `rev${i}`
300
+ }))
301
+ }
302
+ }
303
+
304
+ const parsed = parseResults(client, result, doctype, { isSingleDoc: true })
305
+
306
+ expect(parsed.data).toEqual({
307
+ _id: 'id0',
308
+ id: 'id0',
309
+ _rev: 'rev0',
310
+ _type: doctype,
311
+ name: 'doc0'
312
+ })
313
+ })
314
+
315
+ it('should return empty data array when no rows are present', () => {
316
+ const result = { rows: { length: 0, item: jest.fn() } }
317
+
318
+ const parsed = parseResults(client, result, doctype)
319
+
320
+ expect(parsed).toEqual({ data: [] })
321
+ })
322
+
323
+ it('should set next=true when limit matches parsed length', () => {
324
+ const result = {
325
+ rows: {
326
+ length: 3,
327
+ item: jest.fn().mockImplementation(i => ({
328
+ data: JSON.stringify({ name: `doc${i}` }),
329
+ doc_id: `id${i}`,
330
+ rev: `rev${i}`
331
+ }))
332
+ }
333
+ }
334
+
335
+ const parsed = parseResults(client, result, doctype, { limit: 3 })
336
+
337
+ expect(parsed.next).toBe(true)
338
+ expect(parsed.data.length).toBe(3)
339
+ })
340
+
341
+ it('should set next=true when there is as much docs as specified limit ', () => {
342
+ const result = {
343
+ rows: {
344
+ length: 3,
345
+ item: jest.fn().mockImplementation(i => ({
346
+ data: JSON.stringify({ name: `doc${i}` }),
347
+ doc_id: `id${i}`,
348
+ rev: `rev${i}`
349
+ }))
350
+ }
351
+ }
352
+
353
+ const parsed = parseResults(client, result, doctype, { limit: 3 })
354
+
355
+ expect(parsed.next).toBe(true)
356
+ expect(parsed.data.length).toBe(3)
357
+ })
358
+
359
+ it('should set next=false when there are less docs than specified limit', () => {
360
+ const result = {
361
+ rows: {
362
+ length: 3,
363
+ item: jest.fn().mockImplementation(i => ({
364
+ data: JSON.stringify({ name: `doc${i}` }),
365
+ doc_id: `id${i}`,
366
+ rev: `rev${i}`
367
+ }))
368
+ }
369
+ }
370
+
371
+ const parsed = parseResults(client, result, doctype, { limit: 4 })
372
+
373
+ expect(parsed.next).toBe(false)
374
+ expect(parsed.data.length).toBe(3)
375
+ })
376
+
377
+ it('should set next=false when there is no limit', () => {
378
+ const result = {
379
+ rows: {
380
+ length: 3,
381
+ item: jest.fn().mockImplementation(i => ({
382
+ data: JSON.stringify({ name: `doc${i}` }),
383
+ doc_id: `id${i}`,
384
+ rev: `rev${i}`
385
+ }))
386
+ }
387
+ }
388
+
389
+ const parsed1 = parseResults(client, result, doctype, { limit: -1 })
390
+ expect(parsed1.next).toBe(false)
391
+ expect(parsed1.data.length).toBe(3)
392
+ const parsed2 = parseResults(client, result, doctype)
393
+ expect(parsed2.next).toBe(false)
394
+ expect(parsed2.data.length).toBe(3)
395
+ })
396
+
397
+ it('should handle single document correctly', () => {
398
+ const result = {
399
+ rows: {
400
+ length: 1,
401
+ item: jest.fn().mockReturnValue({
402
+ data: JSON.stringify({ name: 'single_doc' }),
403
+ doc_id: 'single_id',
404
+ rev: 'single_rev'
405
+ })
406
+ }
407
+ }
408
+
409
+ const parsed = parseResults(client, result, doctype, { isSingleDoc: true })
410
+
411
+ expect(parsed.data).toEqual({
412
+ _id: 'single_id',
413
+ id: 'single_id',
414
+ _rev: 'single_rev',
415
+ _type: doctype,
416
+ name: 'single_doc'
417
+ })
418
+ })
419
+ })
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.default = void 0;
9
+
10
+ var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
11
+
12
+ var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
13
+
14
+ var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
15
+
16
+ var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
17
+
18
+ var _dbInterface = _interopRequireDefault(require("../dbInterface"));
19
+
20
+ function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = (0, _getPrototypeOf2.default)(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2.default)(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2.default)(this, result); }; }
21
+
22
+ function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
23
+
24
+ var SQLiteQueryEngine = /*#__PURE__*/function (_DatabaseQueryEngine) {
25
+ (0, _inherits2.default)(SQLiteQueryEngine, _DatabaseQueryEngine);
26
+
27
+ var _super = _createSuper(SQLiteQueryEngine);
28
+
29
+ function SQLiteQueryEngine(pouchManager, doctype) {
30
+ var _this;
31
+
32
+ (0, _classCallCheck2.default)(this, SQLiteQueryEngine);
33
+ _this = _super.call(this);
34
+ throw new Error('SQLiteQueryEngine is not implemented for non-native environments');
35
+ return _this;
36
+ }
37
+
38
+ return SQLiteQueryEngine;
39
+ }(_dbInterface.default);
40
+
41
+ exports.default = SQLiteQueryEngine;