cozy-pouch-link 60.29.0 → 60.30.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.
@@ -130,10 +130,20 @@ var parseResults = function parseResults(client, result, doctype) {
130
130
  skip: skip,
131
131
  next: next
132
132
  };
133
- };
133
+ }; // Quote a value for inline SQL. String single quotes are doubled ('' ) so a
134
+ // value like "l'ete.txt" cannot break out of the literal (SQL error) or inject.
135
+
134
136
 
135
137
  exports.parseResults = parseResults;
136
138
 
139
+ var quoteSQLValue = function quoteSQLValue(value) {
140
+ if (typeof value === 'string') {
141
+ return "'".concat(value.replace(/'/g, "''"), "'");
142
+ }
143
+
144
+ return value;
145
+ };
146
+
137
147
  var parseCondition = function parseCondition(field, condition) {
138
148
  var conditions = [];
139
149
  var sqlField = transformMangoFieldInJSONSQL(field);
@@ -143,10 +153,16 @@ var parseCondition = function parseCondition(field, condition) {
143
153
  var sqlOp = MANGO_TO_SQL_OP[operator];
144
154
 
145
155
  if (operator === '$in' || operator === '$nin') {
146
- var values = condition[operator].map(function (v) {
147
- return typeof v === 'string' ? "'".concat(v, "'") : v;
148
- }).join(', ');
149
- conditions.push("".concat(sqlField, " ").concat(sqlOp, " (").concat(values, ")"));
156
+ var list = condition[operator] || [];
157
+
158
+ if (list.length === 0) {
159
+ // "IN ()" is a syntax error. $in [] matches nothing (0 = false),
160
+ // $nin [] matches everything (1 = true).
161
+ conditions.push(operator === '$in' ? '0' : '1');
162
+ } else {
163
+ var values = list.map(quoteSQLValue).join(', ');
164
+ conditions.push("".concat(sqlField, " ").concat(sqlOp, " (").concat(values, ")"));
165
+ }
150
166
  } else if (operator === '$exists') {
151
167
  var value = condition[operator];
152
168
 
@@ -158,20 +174,16 @@ var parseCondition = function parseCondition(field, condition) {
158
174
 
159
175
  conditions.push("".concat(sqlField, " ").concat(sqlOp));
160
176
  } else {
161
- var _value = typeof condition[operator] === 'string' ? "'".concat(condition[operator], "'") : condition[operator];
162
-
163
- if (operator === '$gt' && _value === null) {
177
+ if (operator === '$gt' && condition[operator] === null) {
164
178
  // Special case for $gt: null conditions
165
179
  conditions.push("".concat(sqlField, " IS NOT NULL"));
166
180
  } else {
167
- conditions.push("".concat(sqlField, " ").concat(sqlOp, " ").concat(_value));
181
+ conditions.push("".concat(sqlField, " ").concat(sqlOp, " ").concat(quoteSQLValue(condition[operator])));
168
182
  }
169
183
  }
170
184
  }
171
185
  } else {
172
- var _value2 = typeof condition === 'string' ? "'".concat(condition, "'") : condition;
173
-
174
- conditions.push("".concat(sqlField, " = ").concat(_value2));
186
+ conditions.push("".concat(sqlField, " = ").concat(quoteSQLValue(condition)));
175
187
  }
176
188
 
177
189
  return conditions.join(' AND ');
@@ -214,7 +226,15 @@ var makeWhereClause = function makeWhereClause(selector) {
214
226
  }
215
227
 
216
228
  var mangoWhere = mangoSelectorToSQL(selector);
217
- baseWhere += " AND ".concat(mangoWhere);
229
+
230
+ if (!mangoWhere) {
231
+ return baseWhere;
232
+ } // Parenthesise the mango expression: SQL binds AND tighter than OR, so a
233
+ // top-level $or would otherwise read as "(DELETED = 0 AND a) OR b" and leak
234
+ // deleted docs matching b.
235
+
236
+
237
+ baseWhere += " AND (".concat(mangoWhere, ")");
218
238
  return baseWhere;
219
239
  };
220
240
 
@@ -223,16 +243,15 @@ exports.makeWhereClause = makeWhereClause;
223
243
  var makeSortClause = function makeSortClause(mangoSortBy) {
224
244
  if (!mangoSortBy || !Array.isArray(mangoSortBy) || mangoSortBy.length < 1) {
225
245
  return null;
226
- }
246
+ } // Each field carries its own direction; a shared trailing ASC/DESC would sort
247
+ // every field the first entry's way and silently ignore mixed asc/desc sorts.
248
+
227
249
 
228
- var firstSortEntry = mangoSortBy[0];
229
- var sortOrder = Object.values(firstSortEntry)[0].toUpperCase();
230
- var sortFields = mangoSortBy.map(function (sort) {
250
+ return mangoSortBy.map(function (sort) {
231
251
  var attribute = Object.keys(sort)[0];
232
- return "json_extract(data, '$.".concat(attribute, "')");
252
+ var order = String(sort[attribute]).toUpperCase();
253
+ return "json_extract(data, '$.".concat(attribute, "') ").concat(order);
233
254
  }).join(', ');
234
- var sortClause = "".concat(sortFields, " ").concat(sortOrder);
235
- return sortClause;
236
255
  };
237
256
 
238
257
  exports.makeSortClause = makeSortClause;
@@ -246,8 +265,12 @@ var makeSQLQueryFromMango = function makeSQLQueryFromMango(_ref2) {
246
265
  _ref2$skip = _ref2.skip,
247
266
  skip = _ref2$skip === void 0 ? 0 : _ref2$skip;
248
267
  var whereClause = makeWhereClause(selector);
249
- var sortClause = makeSortClause(sort);
250
- var sql = ["SELECT json AS data, doc_id, rev", "FROM 'by-sequence' INDEXED BY ".concat(indexName), "WHERE ".concat(whereClause)].join(' ');
268
+ var sortClause = makeSortClause(sort); // Scan by-sequence via the mango index, then join document-store on its
269
+ // winningseq so only WINNING revisions survive (matches CouchDB mango, which
270
+ // queries the winning rev). `data` aliases 'by-sequence'.json; `deleted` lives
271
+ // only on by-sequence, so the where clause stays unambiguous.
272
+
273
+ var sql = ["SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev", "FROM 'by-sequence' INDEXED BY ".concat(indexName, ", 'document-store'"), "WHERE 'by-sequence'.seq = 'document-store'.winningseq AND ".concat(whereClause)].join(' ');
251
274
 
252
275
  if (skip > 0) {
253
276
  sql += " OFFSET ".concat(skip);
@@ -265,15 +288,21 @@ var makeSQLQueryFromMango = function makeSQLQueryFromMango(_ref2) {
265
288
  exports.makeSQLQueryFromMango = makeSQLQueryFromMango;
266
289
 
267
290
  var makeSQLQueryForId = function makeSQLQueryForId(id) {
268
- var sql = ["SELECT json AS data, doc_id, rev", "FROM 'by-sequence'", "WHERE doc_id=\"".concat(id, "\" AND deleted=0"), ";"].join(' ');
291
+ // Join document-store on its winningseq so we return the WINNING revision
292
+ // (PouchDB keeps every rev in by-sequence; document-store.winningseq points at
293
+ // the winning one). Both sides are indexed: by-sequence.seq is the PK and the
294
+ // adapter indexes document-store.winningseq.
295
+ var sql = ["SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev", "FROM 'document-store', 'by-sequence'", "WHERE 'by-sequence'.seq = 'document-store'.winningseq", "AND 'document-store'.id = \"".concat(id, "\" AND 'by-sequence'.deleted = 0"), ";"].join(' ');
269
296
  return sql;
270
297
  };
271
298
 
272
299
  exports.makeSQLQueryForId = makeSQLQueryForId;
273
300
 
274
301
  var makeSQLQueryForIds = function makeSQLQueryForIds(ids) {
275
- var doc_ids = ids.join(',');
276
- var sql = "\n SELECT json AS data, doc_id, rev\n FROM 'by-sequence'\n WHERE doc_id IN (\"".concat(doc_ids, "\") AND deleted = 0;\n ");
302
+ var doc_ids = ids.map(function (id) {
303
+ return "\"".concat(id, "\"");
304
+ }).join(', ');
305
+ var sql = "\n SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev\n FROM 'document-store', 'by-sequence'\n WHERE 'by-sequence'.seq = 'document-store'.winningseq\n AND 'document-store'.id IN (".concat(doc_ids, ") AND 'by-sequence'.deleted = 0;\n ");
277
306
  return sql;
278
307
  };
279
308
 
@@ -286,7 +315,7 @@ var makeSQLQueryAll = function makeSQLQueryAll() {
286
315
  _ref3$skip = _ref3.skip,
287
316
  skip = _ref3$skip === void 0 ? 0 : _ref3$skip;
288
317
 
289
- var sql = ["SELECT json AS data, doc_id, rev", "FROM 'by-sequence'", "WHERE deleted=0", "LIMIT ".concat(limit)].join(' ');
318
+ var sql = ["SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev", "FROM 'document-store', 'by-sequence'", "WHERE 'by-sequence'.seq = 'document-store'.winningseq AND 'by-sequence'.deleted=0", "LIMIT ".concat(limit)].join(' ');
290
319
 
291
320
  if (skip > 0) {
292
321
  sql += " OFFSET ".concat(skip);
@@ -324,8 +353,11 @@ var makeSQLCreateMangoIndex = function makeSQLCreateMangoIndex(indexName, fields
324
353
  exports.makeSQLCreateMangoIndex = makeSQLCreateMangoIndex;
325
354
 
326
355
  var makeSQLCreateDocIDIndex = function makeSQLCreateDocIDIndex() {
327
- // This index is useful for docid queries
328
- var sql = "\n CREATE UNIQUE INDEX IF NOT EXISTS 'by_docid_and_deleted'\n ON 'by-sequence'\n (doc_id, deleted);\n ";
356
+ // This index is useful for docid queries. It is NOT unique: PouchDB's
357
+ // by-sequence keeps a doc's whole rev history, so several rows can share the
358
+ // same (doc_id, deleted) — the winning rev is picked in JS by
359
+ // keepDocWitHighestRev(). A UNIQUE index here throws on any multi-rev doc.
360
+ var sql = "\n CREATE INDEX IF NOT EXISTS 'by_docid_and_deleted'\n ON 'by-sequence'\n (doc_id, deleted);\n ";
329
361
  return sql;
330
362
  };
331
363
 
@@ -404,7 +436,26 @@ var executeSQL = /*#__PURE__*/function () {
404
436
  while (1) {
405
437
  switch (_context3.prev = _context3.next) {
406
438
  case 0:
407
- return _context3.abrupt("return", db.executeAsync(sql));
439
+ return _context3.abrupt("return", db.executeAsync(sql).catch(function (err) {
440
+ var message = err && err.message || String(err); // Before the adapter has created the by-sequence table, the earliest
441
+ // queries hit a missing table. That is transient (the table appears once
442
+ // replication first writes), so return empty instead of rejecting uncaught.
443
+ // Lock timeouts are NOT swallowed: WAL + busy_timeout handle contention, and
444
+ // a genuine lock error must surface rather than look like "no documents".
445
+
446
+ if (/no such table/i.test(message)) {
447
+ return {
448
+ rows: {
449
+ length: 0,
450
+ item: function item() {
451
+ return undefined;
452
+ }
453
+ }
454
+ };
455
+ }
456
+
457
+ throw err;
458
+ }));
408
459
 
409
460
  case 1:
410
461
  case "end":
@@ -0,0 +1,90 @@
1
+ import { makeWhereClause, makeSortClause, makeSQLQueryForIds } from './sql'
2
+
3
+ // Compatibility of the native SQLite engine with the mango selector language
4
+ // (cozy-client / pouchdb-find).
5
+ //
6
+ // - "correctness fixes" lock the behaviour of selectors the engine DOES accept
7
+ // but used to mistranslate (quoting, $or precedence, empty $in, sort order).
8
+ // - "gaps" document selectors mango accepts that the engine cannot translate
9
+ // yet; they are `.skip` and assert the SQL we want, so each unskips the day
10
+ // the gap is closed. See the tracking issue for the full matrix.
11
+
12
+ describe('native SQLite mango — correctness fixes', () => {
13
+ it('escapes single quotes in string values (no SQL break / injection)', () => {
14
+ expect(makeWhereClause({ name: { $eq: "l'ete.txt" } })).toBe(
15
+ "DELETED = 0 AND (json_extract(data, '$.name') = 'l''ete.txt')"
16
+ )
17
+ })
18
+
19
+ it('parenthesises the mango expression so a top-level $or keeps the DELETED guard', () => {
20
+ expect(
21
+ makeWhereClause({ $or: [{ type: 'file' }, { type: 'directory' }] })
22
+ ).toBe(
23
+ "DELETED = 0 AND ((json_extract(data, '$.type') = 'file') OR (json_extract(data, '$.type') = 'directory'))"
24
+ )
25
+ })
26
+
27
+ it('turns an empty $in into a false constant instead of "IN ()"', () => {
28
+ expect(makeWhereClause({ type: { $in: [] } })).toBe('DELETED = 0 AND (0)')
29
+ })
30
+
31
+ it('turns an empty $nin into a true constant', () => {
32
+ expect(makeWhereClause({ type: { $nin: [] } })).toBe('DELETED = 0 AND (1)')
33
+ })
34
+
35
+ it('applies per-field sort direction for mixed asc/desc', () => {
36
+ expect(makeSortClause([{ name: 'asc' }, { size: 'desc' }])).toBe(
37
+ "json_extract(data, '$.name') ASC, json_extract(data, '$.size') DESC"
38
+ )
39
+ })
40
+
41
+ it('quotes each id separately in getByIds', () => {
42
+ expect(makeSQLQueryForIds(['a', 'b'])).toContain('IN ("a", "b")')
43
+ })
44
+ })
45
+
46
+ describe('native SQLite mango — gaps (mango accepts, engine does not yet)', () => {
47
+ // $not / $nor are currently read as field names -> "json_extract('$.$not') undefined"
48
+ it.skip('translates $not', () => {
49
+ expect(makeWhereClause({ $not: { type: 'file' } })).not.toContain(
50
+ 'undefined'
51
+ )
52
+ })
53
+
54
+ // No mapping in MANGO_TO_SQL_OP -> literal "undefined" in the SQL
55
+ it.skip('translates $regex', () => {
56
+ expect(makeWhereClause({ name: { $regex: '^foo' } })).not.toContain(
57
+ 'undefined'
58
+ )
59
+ })
60
+
61
+ // Array operators are entirely absent
62
+ it.skip('translates $elemMatch on an array field', () => {
63
+ expect(
64
+ makeWhereClause({ referenced_by: { $elemMatch: { type: 'album' } } })
65
+ ).not.toContain('undefined')
66
+ })
67
+ it.skip('translates $all', () => {
68
+ expect(makeWhereClause({ tags: { $all: ['a', 'b'] } })).not.toContain(
69
+ 'undefined'
70
+ )
71
+ })
72
+ it.skip('translates $size', () => {
73
+ expect(makeWhereClause({ tags: { $size: 3 } })).not.toContain('undefined')
74
+ })
75
+
76
+ // A nested object means subfield equality; the sub-keys are currently iterated
77
+ // as operators -> "json_extract('$.cozyMetadata') undefined 'drive'"
78
+ it.skip('treats a nested object as subfield equality', () => {
79
+ expect(makeWhereClause({ cozyMetadata: { createdByApp: 'drive' } })).toBe(
80
+ "DELETED = 0 AND (json_extract(data, '$.cozyMetadata.createdByApp') = 'drive')"
81
+ )
82
+ })
83
+
84
+ // "= NULL" never matches; mango $eq null should match null / missing
85
+ it.skip('translates $eq null to an IS NULL check', () => {
86
+ expect(makeWhereClause({ trashed: { $eq: null } })).toBe(
87
+ "DELETED = 0 AND (json_extract(data, '$.trashed') IS NULL)"
88
+ )
89
+ })
90
+ })
@@ -3,6 +3,8 @@ import {
3
3
  makeWhereClause,
4
4
  makeSortClause,
5
5
  makeSQLQueryFromMango,
6
+ makeSQLQueryForId,
7
+ makeSQLQueryForIds,
6
8
  keepDocWitHighestRev,
7
9
  makeSQLQueryAll,
8
10
  parseResults
@@ -113,7 +115,7 @@ describe('makeWhereClause', () => {
113
115
  it('should return deleted and mango clauses when there is a mango selector', () => {
114
116
  const selector = { status: 'active' }
115
117
  expect(makeWhereClause(selector)).toEqual(
116
- "DELETED = 0 AND json_extract(data, '$.status') = 'active'"
118
+ "DELETED = 0 AND (json_extract(data, '$.status') = 'active')"
117
119
  )
118
120
  })
119
121
  })
@@ -132,7 +134,7 @@ describe('makeSortClause', () => {
132
134
  it('should return correct order by, with multiple sorting attribute', () => {
133
135
  const sortBy = [{ date: 'asc' }, { name: 'asc' }, { type: 'asc' }]
134
136
  expect(makeSortClause(sortBy)).toEqual(
135
- "json_extract(data, '$.date'), json_extract(data, '$.name'), json_extract(data, '$.type') ASC"
137
+ "json_extract(data, '$.date') ASC, json_extract(data, '$.name') ASC, json_extract(data, '$.type') ASC"
136
138
  )
137
139
  })
138
140
 
@@ -152,9 +154,9 @@ describe('makeSQLQueryFromMango', () => {
152
154
  const sql = makeSQLQueryFromMango({ selector, indexName, limit })
153
155
 
154
156
  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'`,
157
+ `SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev`,
158
+ `FROM 'by-sequence' INDEXED BY by_name, 'document-store'`,
159
+ `WHERE 'by-sequence'.seq = 'document-store'.winningseq AND DELETED = 0 AND (json_extract(data, '$.date') > '2025-01-01')`,
158
160
  `LIMIT 100;`
159
161
  ].join(' ')
160
162
  expect(sql).toEqual(expectedSql)
@@ -168,9 +170,9 @@ describe('makeSQLQueryFromMango', () => {
168
170
  const sql = makeSQLQueryFromMango({ selector, sort, indexName, limit })
169
171
 
170
172
  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'`,
173
+ `SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev`,
174
+ `FROM 'by-sequence' INDEXED BY by_name, 'document-store'`,
175
+ `WHERE 'by-sequence'.seq = 'document-store'.winningseq AND DELETED = 0 AND (json_extract(data, '$.date') > '2025-01-01')`,
174
176
  `ORDER BY json_extract(data, '$.date') ASC`,
175
177
  `LIMIT 100;`
176
178
  ].join(' ')
@@ -190,9 +192,9 @@ describe('makeSQLQueryFromMango', () => {
190
192
  })
191
193
 
192
194
  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'`,
195
+ `SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev`,
196
+ `FROM 'by-sequence' INDEXED BY by_name, 'document-store'`,
197
+ `WHERE 'by-sequence'.seq = 'document-store'.winningseq AND DELETED = 0 AND (json_extract(data, '$.date') > '2025-01-01')`,
196
198
  `OFFSET 100`,
197
199
  `LIMIT 200;`
198
200
  ].join(' ')
@@ -204,9 +206,9 @@ describe('makeSQLQueryAll', () => {
204
206
  it('should return a correct sql query to get all docs', () => {
205
207
  const sql = makeSQLQueryAll()
206
208
  const expectedSql = [
207
- `SELECT json AS data, doc_id, rev`,
208
- `FROM 'by-sequence'`,
209
- `WHERE deleted=0`,
209
+ `SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev`,
210
+ `FROM 'document-store', 'by-sequence'`,
211
+ `WHERE 'by-sequence'.seq = 'document-store'.winningseq AND 'by-sequence'.deleted=0`,
210
212
  `LIMIT -1;`
211
213
  ].join(' ')
212
214
  expect(sql).toEqual(expectedSql)
@@ -215,9 +217,9 @@ describe('makeSQLQueryAll', () => {
215
217
  it('should handle limit and skip', () => {
216
218
  const sql = makeSQLQueryAll({ limit: 10, skip: 100 })
217
219
  const expectedSql = [
218
- `SELECT json AS data, doc_id, rev`,
219
- `FROM 'by-sequence'`,
220
- `WHERE deleted=0`,
220
+ `SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev`,
221
+ `FROM 'document-store', 'by-sequence'`,
222
+ `WHERE 'by-sequence'.seq = 'document-store'.winningseq AND 'by-sequence'.deleted=0`,
221
223
  `LIMIT 10`,
222
224
  `OFFSET 100;`
223
225
  ].join(' ')
@@ -225,6 +227,22 @@ describe('makeSQLQueryAll', () => {
225
227
  })
226
228
  })
227
229
 
230
+ describe('makeSQLQueryForId', () => {
231
+ it('joins document-store on winningseq to return the winning revision', () => {
232
+ const sql = makeSQLQueryForId('abc')
233
+ expect(sql).toContain(`'by-sequence'.seq = 'document-store'.winningseq`)
234
+ expect(sql).toContain(`'document-store'.id = "abc"`)
235
+ })
236
+ })
237
+
238
+ describe('makeSQLQueryForIds', () => {
239
+ it('emits one quoted value per id in the IN clause', () => {
240
+ const sql = makeSQLQueryForIds(['a', 'b'])
241
+ expect(sql).toContain(`'document-store'.id IN ("a", "b")`)
242
+ expect(sql).toContain(`'by-sequence'.seq = 'document-store'.winningseq`)
243
+ })
244
+ })
245
+
228
246
  describe('keepDocWitHighestRev', () => {
229
247
  it('should return null if no docs', () => {
230
248
  expect(keepDocWitHighestRev([])).toBeNull()
@@ -56,20 +56,37 @@ var SQLiteQueryEngine = /*#__PURE__*/function (_DatabaseQueryEngine) {
56
56
  (0, _createClass2.default)(SQLiteQueryEngine, [{
57
57
  key: "openDB",
58
58
  value: function openDB(dbName) {
59
- var fileDbName = "".concat(dbName, ".sqlite");
60
-
61
- try {
62
- this.db = (0, _opSqlite.open)({
63
- name: fileDbName
64
- }); // Create index at db opening if needed
65
-
66
- var docIdIndexSql = (0, _sql.makeSQLCreateDocIDIndex)();
67
- var deletedIndexSql = (0, _sql.makeSQLCreateDeletedIndex)();
68
- (0, _sql.executeSQL)(this.db, docIdIndexSql);
69
- (0, _sql.executeSQL)(this.db, deletedIndexSql);
70
- } catch (err) {
71
- _logger.default.error(err);
72
- }
59
+ var fileDbName = "".concat(dbName, ".sqlite"); // Resolve the DB handle lazily on first use, opening our OWN op-sqlite
60
+ // connection on the same file the adapter uses. WAL mode + a busy timeout
61
+ // let this connection coexist with the adapter's writer without "database
62
+ // is locked" — the two-connection setup in rollback mode was what made this
63
+ // engine unusable. Lazy so the adapter has created the schema by the time
64
+ // the first query runs.
65
+
66
+ var resolved = null;
67
+ Object.defineProperty(this, 'db', {
68
+ configurable: true,
69
+ set: function set(value) {
70
+ resolved = value;
71
+ },
72
+ get: function get() {
73
+ if (resolved) return resolved;
74
+ resolved = (0, _opSqlite.open)({
75
+ name: fileDbName
76
+ });
77
+
78
+ try {
79
+ (0, _sql.executeSQL)(resolved, 'PRAGMA journal_mode=WAL');
80
+ (0, _sql.executeSQL)(resolved, 'PRAGMA busy_timeout=5000');
81
+ (0, _sql.executeSQL)(resolved, (0, _sql.makeSQLCreateDocIDIndex)());
82
+ (0, _sql.executeSQL)(resolved, (0, _sql.makeSQLCreateDeletedIndex)());
83
+ } catch (err) {
84
+ _logger.default.error(err);
85
+ }
86
+
87
+ return resolved;
88
+ }
89
+ });
73
90
  }
74
91
  }, {
75
92
  key: "allDocs",
package/dist/jsonapi.js CHANGED
@@ -63,6 +63,18 @@ var resetAllPaths = function resetAllPaths() {
63
63
  exports.resetAllPaths = resetAllPaths;
64
64
 
65
65
  var normalizeDocs = function normalizeDocs(client, doctype, docs) {
66
+ if (doctype === 'io.cozy.files') {
67
+ // Seed the path cache with every directory's path first, so each file
68
+ // resolves its parent path from memory instead of a per-file getById.
69
+ for (var j = 0; j < docs.length; j++) {
70
+ var dir = docs[j];
71
+
72
+ if (dir && dir.type === _files.TYPE_DIRECTORY && dir.path) {
73
+ setFilePath(dir._id || dir.id, dir.path);
74
+ }
75
+ }
76
+ }
77
+
66
78
  for (var i = docs.length; i >= 0; i--) {
67
79
  var doc = docs[i];
68
80
 
@@ -4,6 +4,7 @@ import {
4
4
  computeFileFullpath,
5
5
  fromPouchResult,
6
6
  normalizeDoc,
7
+ normalizeDocs,
7
8
  resetAllPaths
8
9
  } from './jsonapi'
9
10
  import { queryFileById } from './files'
@@ -720,6 +721,18 @@ describe('computeFileFullpath', () => {
720
721
  expect(res2.path).toEqual('ROOT/MYDIR/file3.1')
721
722
  })
722
723
 
724
+ it('normalizeDocs seeds directory paths so sibling files resolve without querying', async () => {
725
+ // Clear the module-level path cache so the result depends on the seeding
726
+ // block, not on a directory a previous test may have cached.
727
+ resetAllPaths()
728
+ queryFileById.mockClear()
729
+ const dirDoc = { ...dir }
730
+ const fileDoc = { ...filewithNoPath }
731
+ normalizeDocs(client, 'io.cozy.files', [dirDoc, fileDoc])
732
+ expect(fileDoc.path).toEqual('ROOT/MYDIR/file3')
733
+ expect(queryFileById).not.toHaveBeenCalled()
734
+ })
735
+
723
736
  it('should return the file unchanged when queryFileById resolves to undefined', async () => {
724
737
  // This happens offline when the link chain returns nothing for the
725
738
  // parent dir lookup. Before the fix, destructuring `.data` from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-pouch-link",
3
- "version": "60.29.0",
3
+ "version": "60.30.1",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "types/index.d.ts",
@@ -13,7 +13,7 @@
13
13
  "url": "git+https://github.com/linagora/cozy-client.git"
14
14
  },
15
15
  "dependencies": {
16
- "cozy-client": "^60.29.0",
16
+ "cozy-client": "^60.30.1",
17
17
  "pouchdb-browser": "^7.2.2",
18
18
  "pouchdb-find": "^7.2.2"
19
19
  },
@@ -41,5 +41,5 @@
41
41
  "typecheck": "tsc -p tsconfig.json"
42
42
  },
43
43
  "sideEffects": false,
44
- "gitHead": "0e624ae6b3ba93c2618ba9d26f4d4f48945bf5c8"
44
+ "gitHead": "f209689b3951ddbedc83db50a2dcd976082777b3"
45
45
  }
@@ -1,6 +1,6 @@
1
1
  export default class SQLiteQueryEngine extends DatabaseQueryEngine {
2
2
  constructor(pouchManager: any, doctype: any);
3
- db: import("@op-engineering/op-sqlite").DB;
3
+ db: any;
4
4
  client: any;
5
5
  doctype: any;
6
6
  }