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.
- package/dist/db/sqlite/sql.js +80 -29
- package/dist/db/sqlite/sql.mango.spec.js +90 -0
- package/dist/db/sqlite/sql.spec.js +35 -17
- package/dist/db/sqlite/sqliteDb.native.js +31 -14
- package/dist/jsonapi.js +12 -0
- package/dist/jsonapi.spec.js +13 -0
- package/package.json +3 -3
- package/types/db/sqlite/sqliteDb.native.d.ts +1 -1
package/dist/db/sqlite/sql.js
CHANGED
|
@@ -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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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(
|
|
181
|
+
conditions.push("".concat(sqlField, " ").concat(sqlOp, " ").concat(quoteSQLValue(condition[operator])));
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
}
|
|
171
185
|
} else {
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
(
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
package/dist/jsonapi.spec.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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": "
|
|
44
|
+
"gitHead": "f209689b3951ddbedc83db50a2dcd976082777b3"
|
|
45
45
|
}
|