UNPKG

cozy-dataproxy-lib

Version:

Library meant to be by Cozy Cloud's DataProxy apps for data manipulation

966 lines (782 loc) 34.6 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.SearchEngine = void 0; var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _cozyClient = require("cozy-client"); var _cozyMinilog = _interopRequireDefault(require("cozy-minilog")); var _cozyRealtime = require("cozy-realtime"); var _consts = require("./consts"); var _client = require("./helpers/client"); var _normalizeSearchResult = require("./helpers/normalizeSearchResult"); var _utils = require("./helpers/utils"); var _indexDocs = require("./indexDocs"); var _queries = require("./queries"); var _storage = require("./storage"); var _types = require("./types"); function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } var log = (0, _cozyMinilog.default)('🗂️ [Indexing]'); var SearchEngine = /*#__PURE__*/function () { function SearchEngine(client, storage, performanceApi) { var _this = this; var engineOptions = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; (0, _classCallCheck2.default)(this, SearchEngine); (0, _defineProperty2.default)(this, "client", void 0); (0, _defineProperty2.default)(this, "searchIndexes", void 0); (0, _defineProperty2.default)(this, "debouncedReplication", void 0); (0, _defineProperty2.default)(this, "isLocalSearch", void 0); (0, _defineProperty2.default)(this, "storage", void 0); (0, _defineProperty2.default)(this, "performanceApi", void 0); (0, _defineProperty2.default)(this, "engineOptions", void 0); this.client = client; this.searchIndexes = {}; this.storage = storage; this.performanceApi = performanceApi !== null && performanceApi !== void 0 ? performanceApi : _cozyClient.defaultPerformanceApi; this.engineOptions = _objectSpread({ shouldInit: true }, engineOptions); this.isLocalSearch = !!(0, _client.getPouchLink)(this.client); log.info('Use local data on trusted device: ', this.isLocalSearch); this.debouncedReplication = function () { var pouchLink = (0, _client.getPouchLink)(client); if (pouchLink) { pouchLink.startReplicationWithDebounce(); } }; if (this.client.isLogged) { this.afterLogin(); } else { this.client.on('login', function () { _this.afterLogin(); }); } } (0, _createClass2.default)(SearchEngine, [{ key: "afterLogin", value: function afterLogin() { if (this.engineOptions.shouldInit) { void this.init(); } // Use replication events to have up-to-date search indexes, based on local data this.handleReplicationEvents(); } }, { key: "indexDocumentsAtInit", value: function () { var _indexDocumentsAtInit = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() { var markName, lastExportDate, _iterator, _step, doctype, searchIndex; return _regenerator.default.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: if (this.client) { _context.next = 2; break; } return _context.abrupt("return"); case 2: log.info('Initialize indexes...'); markName = this.performanceApi.mark('indexDocuments'); _context.next = 6; return (0, _storage.getExportDate)(this.storage); case 6: lastExportDate = _context.sent; if (!(!lastExportDate || !this.isLocalSearch)) { _context.next = 29; break; } // No persisted index: let's create them _iterator = _createForOfIteratorHelper(_consts.SEARCHABLE_DOCTYPES); _context.prev = 9; _iterator.s(); case 11: if ((_step = _iterator.n()).done) { _context.next = 19; break; } doctype = _step.value; _context.next = 15; return this.indexDocsForSearch(doctype); case 15: searchIndex = _context.sent; if (searchIndex) { this.searchIndexes[doctype] = searchIndex; } case 17: _context.next = 11; break; case 19: _context.next = 24; break; case 21: _context.prev = 21; _context.t0 = _context["catch"](9); _iterator.e(_context.t0); case 24: _context.prev = 24; _iterator.f(); return _context.finish(24); case 27: _context.next = 31; break; case 29: _context.next = 31; return this.initSearchWithIndexImport(); case 31: this.performanceApi.measure({ markName: markName, category: 'Search' }); case 32: case "end": return _context.stop(); } } }, _callee, this, [[9, 21, 24, 27]]); })); function indexDocumentsAtInit() { return _indexDocumentsAtInit.apply(this, arguments); } return indexDocumentsAtInit; }() }, { key: "handleReplicationEvents", value: function handleReplicationEvents() { var _this2 = this; if (!this.isLocalSearch) { // Nothing to do here, we do not want to replicate for non-local search return; } var startReplicationTime = 0, endReplicationTime = 0; this.client.on('pouchlink:doctypesync:end', /*#__PURE__*/function () { var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(doctype) { var searchIndex; return _regenerator.default.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: if (!((0, _types.isSearchedDoctype)(doctype) && _this2.searchIndexes[doctype])) { _context2.next = 5; break; } _context2.next = 3; return _this2.indexDocsForSearch(doctype); case 3: searchIndex = _context2.sent; if (searchIndex) { _this2.searchIndexes[doctype] = searchIndex; } case 5: case "end": return _context2.stop(); } } }, _callee2); })); return function (_x) { return _ref.apply(this, arguments); }; }()); this.client.on('pouchlink:sync:start', function () { log.debug('Started pouch replication'); startReplicationTime = performance.now(); }); this.client.on('pouchlink:sync:end', /*#__PURE__*/(0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3() { return _regenerator.default.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: log.debug('Ended pouch replication'); endReplicationTime = performance.now(); if (startReplicationTime > 0) { // Log only if the start replication event was correctly get log.debug("Replication took ".concat((endReplicationTime - startReplicationTime).toFixed(2), " ms")); } if (!(Object.keys(_this2.searchIndexes).length < 1)) { _context3.next = 7; break; } log.info('No search index found: start indexing'); // This happens at first replication, so init the indexes _context3.next = 7; return _this2.indexDocumentsAtInit(); case 7: // Save up-to-date index on storage to be later imported void (0, _storage.exportSearchIndexes)(_this2.storage, _this2.searchIndexes); case 8: case "end": return _context3.stop(); } } }, _callee3); }))); } }, { key: "init", value: function () { var _init = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4() { return _regenerator.default.wrap(function _callee4$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: // Ensure login is done before plugin register if (!this.client.plugins[_cozyRealtime.RealtimePlugin.pluginName]) { this.client.registerPlugin(_cozyRealtime.RealtimePlugin, {}); } // Realtime subscription this.handleUpdatedOrCreatedDoc = this.handleUpdatedOrCreatedDoc.bind(this); this.handleDeletedDoc = this.handleDeletedDoc.bind(this); this.subscribeDoctype(this.client, _consts.FILES_DOCTYPE); this.subscribeDoctype(this.client, _consts.CONTACTS_DOCTYPE); this.subscribeDoctype(this.client, _consts.APPS_DOCTYPE); // The document indexing should be performed once everything is setup _context4.next = 8; return this.indexDocumentsAtInit(); case 8: case "end": return _context4.stop(); } } }, _callee4, this); })); function init() { return _init.apply(this, arguments); } return init; }() }, { key: "subscribeDoctype", value: function subscribeDoctype(client, doctype) { /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/unbound-method */ var realtime = client.plugins.realtime; realtime.subscribe('created', doctype, this.handleUpdatedOrCreatedDoc); realtime.subscribe('updated', doctype, this.handleUpdatedOrCreatedDoc); realtime.subscribe('deleted', doctype, this.handleDeletedDoc); /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/unbound-method */ } }, { key: "handleUpdatedOrCreatedDoc", value: function handleUpdatedOrCreatedDoc(doc) { var _this$searchIndexes; var doctype = doc._type; if (!(0, _types.isSearchedDoctype)(doctype)) { return; } var searchIndex = (_this$searchIndexes = this.searchIndexes) === null || _this$searchIndexes === void 0 ? void 0 : _this$searchIndexes[doctype]; if (!searchIndex) { // No index yet: it will be done by querying the local db after first replication return; } log.debug('[REALTIME] Update doc from index after update : ', doc._id); void (0, _indexDocs.indexSingleDoc)(this.client, searchIndex.index, doc); if (this.isLocalSearch) { this.debouncedReplication(); } } }, { key: "handleDeletedDoc", value: function handleDeletedDoc(doc) { var _this$searchIndexes2; var doctype = doc._type; if (!(0, _types.isSearchedDoctype)(doctype)) { return; } var searchIndex = (_this$searchIndexes2 = this.searchIndexes) === null || _this$searchIndexes2 === void 0 ? void 0 : _this$searchIndexes2[doctype]; if (!searchIndex) { // No index yet: it will be done by querying the local db after first replication return; } log.debug('[REALTIME] Remove doc from index after update : ', doc._id); this.searchIndexes[doctype].index.remove(doc._id); if (this.isLocalSearch) { this.debouncedReplication(); } } /** * Initialize indexes by: * - importing persisted indexes * - Requesting remote changes if the indexes are stale * - Query the local database to load the redux store */ }, { key: "initSearchWithIndexImport", value: function () { var _initSearchWithIndexImport = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee5() { var startImport, endImport, _iterator2, _step2, doctype, searchIndex, newSearchIndex; return _regenerator.default.wrap(function _callee5$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: startImport = performance.now(); _context5.next = 3; return (0, _storage.importSearchIndexes)(this.storage); case 3: this.searchIndexes = _context5.sent; endImport = performance.now(); if ((0, _utils.isDebug)()) { log.debug("Index import took ".concat((endImport - startImport).toFixed(2), " ms")); } _iterator2 = _createForOfIteratorHelper(_consts.SEARCHABLE_DOCTYPES); _context5.prev = 7; _iterator2.s(); case 9: if ((_step2 = _iterator2.n()).done) { _context5.next = 22; break; } doctype = _step2.value; searchIndex = this.searchIndexes[doctype]; if (searchIndex) { _context5.next = 17; break; } _context5.next = 15; return this.indexDocsForSearch(doctype); case 15: newSearchIndex = _context5.sent; if (newSearchIndex) { this.searchIndexes[doctype] = newSearchIndex; } case 17: _context5.next = 19; return (0, _indexDocs.indexOnChanges)(this, this.searchIndexes[doctype], doctype); case 19: // The doctype needs some extra treatment to initialize everything correctly void (0, _indexDocs.initDoctypeAfterIndexImport)(this.client, doctype); case 20: _context5.next = 9; break; case 22: _context5.next = 27; break; case 24: _context5.prev = 24; _context5.t0 = _context5["catch"](7); _iterator2.e(_context5.t0); case 27: _context5.prev = 27; _iterator2.f(); return _context5.finish(27); case 30: case "end": return _context5.stop(); } } }, _callee5, this, [[7, 24, 27, 30]]); })); function initSearchWithIndexImport() { return _initSearchWithIndexImport.apply(this, arguments); } return initSearchWithIndexImport; }() }, { key: "buildSearchIndex", value: function buildSearchIndex(doctype, docs) { var startTimeIndex = performance.now(); var flexsearchIndex = (0, _indexDocs.initSearchIndex)(doctype); (0, _indexDocs.indexAllDocs)(flexsearchIndex, docs, this.isLocalSearch); var endTimeIndex = performance.now(); log.debug("Create ".concat(doctype, " index took ").concat((endTimeIndex - startTimeIndex).toFixed(2), " ms")); return flexsearchIndex; } }, { key: "getLocalLastSeq", value: function () { var _getLocalLastSeq = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee6(doctype) { var pouchLink, info; return _regenerator.default.wrap(function _callee6$(_context6) { while (1) { switch (_context6.prev = _context6.next) { case 0: if (!this.isLocalSearch) { _context6.next = 11; break; } pouchLink = (0, _client.getPouchLink)(this.client); if (!pouchLink) { _context6.next = 8; break; } _context6.next = 5; return pouchLink.getDbInfo(doctype); case 5: _context6.t0 = _context6.sent; _context6.next = 9; break; case 8: _context6.t0 = null; case 9: info = _context6.t0; return _context6.abrupt("return", (info === null || info === void 0 ? void 0 : info.update_seq) || 0); case 11: return _context6.abrupt("return", -1); case 12: case "end": return _context6.stop(); } } }, _callee6, this); })); function getLocalLastSeq(_x2) { return _getLocalLastSeq.apply(this, arguments); } return getLocalLastSeq; }() }, { key: "initialIndexation", value: function () { var _initialIndexation = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee7(doctype) { var docs, index, lastSeq; return _regenerator.default.wrap(function _callee7$(_context7) { while (1) { switch (_context7.prev = _context7.next) { case 0: _context7.next = 2; return (0, _queries.queryLocalOrRemoteDocs)(this.client, doctype, { isLocalSearch: this.isLocalSearch }); case 2: docs = _context7.sent; if (!(docs.length < 1)) { _context7.next = 5; break; } return _context7.abrupt("return", null); case 5: index = this.buildSearchIndex(doctype, docs); _context7.next = 8; return this.getLocalLastSeq(doctype); case 8: lastSeq = _context7.sent; this.searchIndexes[doctype] = { index: index, lastSeq: lastSeq, lastUpdated: new Date().toISOString() }; return _context7.abrupt("return", this.searchIndexes[doctype]); case 11: case "end": return _context7.stop(); } } }, _callee7, this); })); function initialIndexation(_x3) { return _initialIndexation.apply(this, arguments); } return initialIndexation; }() }, { key: "incrementalIndexation", value: function () { var _incrementalIndexation = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee8(doctype, searchIndex) { return _regenerator.default.wrap(function _callee8$(_context8) { while (1) { switch (_context8.prev = _context8.next) { case 0: return _context8.abrupt("return", (0, _indexDocs.indexOnChanges)(this, searchIndex, doctype)); case 1: case "end": return _context8.stop(); } } }, _callee8, this); })); function incrementalIndexation(_x4, _x5) { return _incrementalIndexation.apply(this, arguments); } return incrementalIndexation; }() }, { key: "indexDocsForSearch", value: function () { var _indexDocsForSearch = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee9(doctype) { var markeNameIndex, searchIndex, startIndexing, index, endIndexing; return _regenerator.default.wrap(function _callee9$(_context9) { while (1) { switch (_context9.prev = _context9.next) { case 0: markeNameIndex = this.performanceApi.mark("indexDocsForSearch ".concat(doctype)); searchIndex = this.searchIndexes[doctype]; startIndexing = performance.now(); if (searchIndex) { _context9.next = 12; break; } _context9.next = 6; return this.initialIndexation(doctype); case 6: index = _context9.sent; if (index) { _context9.next = 10; break; } this.performanceApi.measure({ markName: markeNameIndex, measureName: "".concat(markeNameIndex, " initial indexation"), category: 'Search' }); return _context9.abrupt("return", null); case 10: _context9.next = 15; break; case 12: _context9.next = 14; return this.incrementalIndexation(doctype, searchIndex); case 14: index = _context9.sent; case 15: endIndexing = performance.now(); if ((0, _utils.isDebug)()) { log.debug("Indexing ".concat(doctype, " took ").concat((endIndexing - startIndexing).toFixed(2), " ms")); } this.performanceApi.measure({ markName: markeNameIndex, measureName: "".concat(markeNameIndex, " incremental indexation"), category: 'Search' }); return _context9.abrupt("return", index); case 19: case "end": return _context9.stop(); } } }, _callee9, this); })); function indexDocsForSearch(_x6) { return _indexDocsForSearch.apply(this, arguments); } return indexDocsForSearch; }() }, { key: "search", value: function () { var _search = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee10(query, options) { var markeNameIndex, allResults, dedupResults, enrichedResults, sortedResults, results, normResults, _iterator3, _step3, res, normalizedRes, output; return _regenerator.default.wrap(function _callee10$(_context10) { while (1) { switch (_context10.prev = _context10.next) { case 0: if (!(!this.searchIndexes || Object.keys(this.searchIndexes).length < 1)) { _context10.next = 3; break; } // The indexing might be running but not finished yet log.warn('[SEARCH] No search index available'); return _context10.abrupt("return", null); case 3: markeNameIndex = this.performanceApi.mark('search'); allResults = this.searchOnIndexes(query, options === null || options === void 0 ? void 0 : options.doctypes); dedupResults = this.deduplicateAndFlatten(allResults); _context10.next = 8; return (0, _normalizeSearchResult.enrichResultsWithDocs)(this.client, dedupResults); case 8: enrichedResults = _context10.sent; sortedResults = this.sortSearchResults(enrichedResults, options === null || options === void 0 ? void 0 : options.doctypes); results = this.limitSearchResults(sortedResults); normResults = []; _iterator3 = _createForOfIteratorHelper(results); try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { res = _step3.value; normalizedRes = (0, _normalizeSearchResult.normalizeSearchResult)(this.client, res, query); normResults.push(normalizedRes); } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } output = normResults.filter(function (res) { return res.title; }); this.performanceApi.measure({ markName: markeNameIndex, category: 'Search' }); return _context10.abrupt("return", output); case 17: case "end": return _context10.stop(); } } }, _callee10, this); })); function search(_x7, _x8) { return _search.apply(this, arguments); } return search; }() }, { key: "searchOnIndexes", value: function searchOnIndexes(query, searchOnDoctypes) { var _this3 = this; var searchResults = []; var _loop = function _loop(key) { var doctype = key; // XXX - Should not be necessary if (searchOnDoctypes && !searchOnDoctypes.includes(doctype)) { // Search only on specified doctypes return "continue"; } var index = _this3.searchIndexes[doctype]; if (!index) { log.warn('[SEARCH] No search index available for ', doctype); return "continue"; } // XXX - The limit is specified twice because of a flexsearch inconstency // that does not enforce the limit if only given in second argument, and // does not return the correct type is only given in third options // // XXX - The given limit here is arbitrary because flexsearch enforce it on matching // field, which can cause issue related to the sort: if we search on name+path for files, // and limit on 100, the 101th result on name will be skipped, but might appear on path, // which will make it appear in the search results, but in incorrect order. // // Search result example: // [ // { // "field": "displayName", // "result": [ // "604627c6bafee013ec5f27f7f72029f6" // ] // }, // { // "field": "fullname", // "result": [ // "604627c6bafee013ec5f27f7f72029f6", "604627c6bafee013ec5f27f3f714568" // ] // } // ] var FLEXSEARCH_LIMIT = 10000; var indexResults = index.index.search(query, FLEXSEARCH_LIMIT, { limit: FLEXSEARCH_LIMIT, enrich: false }); var newResults = indexResults.map(function (res) { return _objectSpread(_objectSpread({}, res), {}, { doctype: doctype }); }); searchResults = searchResults.concat(newResults); }; for (var key in this.searchIndexes) { var _ret = _loop(key); if (_ret === "continue") continue; } return searchResults; } }, { key: "deduplicateAndFlatten", value: function deduplicateAndFlatten(searchResults) { var combinedResults = searchResults.flatMap(function (item) { return item.result.map(function (id) { return { id: id.toString(), // Because of flexsearch Id typing doctype: item.doctype, field: item.field }; }); }); var resultMap = new Map(); combinedResults.forEach(function (_ref3) { var id = _ref3.id, field = _ref3.field, doctype = _ref3.doctype; if (resultMap.has(id)) { var _resultMap$get; (_resultMap$get = resultMap.get(id)) === null || _resultMap$get === void 0 ? void 0 : _resultMap$get.fields.push(field); } else { resultMap.set(id, { id: id, fields: [field], doctype: doctype }); } }); return (0, _toConsumableArray2.default)(resultMap.values()); } }, { key: "compareStrings", value: function compareStrings(str1, str2) { if (!str1 && !str2) { return 0; } else if (!str1) { return 1; } else if (!str2) { return -1; } return str1.localeCompare(str2, undefined, { numeric: true }); } }, { key: "sortSearchResults", value: function sortSearchResults(searchResults, doctypesOrder) { var _this4 = this; return searchResults.sort(function (a, b) { var doctypeComparison; if (doctypesOrder) { doctypeComparison = doctypesOrder.findIndex(function (dt) { return dt === a.doctype; }) - doctypesOrder.findIndex(function (dt) { return dt === b.doctype; }); } else { doctypeComparison = _consts.DOCTYPE_DEFAULT_ORDER[a.doctype] - _consts.DOCTYPE_DEFAULT_ORDER[b.doctype]; } if (doctypeComparison !== 0) return doctypeComparison; if (a.doctype === _consts.APPS_DOCTYPE && (0, _types.isIOCozyApp)(a.doc) && (0, _types.isIOCozyApp)(b.doc)) { var _a$doc, _b$doc; return _this4.compareStrings((_a$doc = a.doc) === null || _a$doc === void 0 ? void 0 : _a$doc.slug, (_b$doc = b.doc) === null || _b$doc === void 0 ? void 0 : _b$doc.slug); } else if (a.doctype === _consts.CONTACTS_DOCTYPE && (0, _types.isIOCozyContact)(a.doc) && (0, _types.isIOCozyContact)(b.doc)) { var _a$doc2, _b$doc2; return _this4.compareStrings((_a$doc2 = a.doc) === null || _a$doc2 === void 0 ? void 0 : _a$doc2.displayName, (_b$doc2 = b.doc) === null || _b$doc2 === void 0 ? void 0 : _b$doc2.displayName); } else if (a.doctype === _consts.FILES_DOCTYPE && (0, _types.isIOCozyFile)(a.doc) && (0, _types.isIOCozyFile)(b.doc)) { return _this4.sortFiles(a, b); } return 0; }); } }, { key: "sortFiles", value: function sortFiles(aRes, bRes) { var _aRes$doc, _bRes$doc; if (!(0, _types.isIOCozyFile)(aRes.doc) || !(0, _types.isIOCozyFile)(bRes.doc)) { return 0; } if (!aRes.fields.includes('name') || !bRes.fields.includes('name')) { // First, sort docs with a match on the name field return aRes.fields.includes('name') ? -1 : 1; } if (aRes.doc.type !== bRes.doc.type) { // Then, directories return aRes.doc.type === 'directory' ? -1 : 1; } // Then name return this.compareStrings((_aRes$doc = aRes.doc) === null || _aRes$doc === void 0 ? void 0 : _aRes$doc.name, (_bRes$doc = bRes.doc) === null || _bRes$doc === void 0 ? void 0 : _bRes$doc.name); } }, { key: "limitSearchResults", value: function limitSearchResults(searchResults) { var doctypesCount = {}; return searchResults.filter(function (result) { var doctype = result.doctype; if (doctypesCount[doctype]) { doctypesCount[doctype] += 1; } else { doctypesCount[doctype] = 1; } return doctypesCount[doctype] <= _consts.LIMIT_DOCTYPE_SEARCH; }); } }]); return SearchEngine; }(); exports.SearchEngine = SearchEngine;