From 6d4fc17d3b25aa8ce6e8c69933e569ede62ccd19 Mon Sep 17 00:00:00 2001 From: Paul Tran-Van Date: Thu, 24 Oct 2024 14:10:12 +0200 Subject: [PATCH] refactor: Make ts/eslint happy --- eslint.config.js | 12 +-- package.json | 3 +- src/@types/cozy-client.d.ts | 4 +- src/@types/cozy-realtime.d.ts | 3 + src/dataproxy/worker/platformWorker.ts | 6 +- src/search/SearchEngine.ts | 70 +++++++++++----- src/search/consts.ts | 9 +- src/search/helpers/replication.spec.ts | 6 +- src/search/types.ts | 24 +++--- yarn.lock | 109 +++++++++++++++++-------- 10 files changed, 167 insertions(+), 79 deletions(-) create mode 100644 src/@types/cozy-realtime.d.ts diff --git a/eslint.config.js b/eslint.config.js index e785e03..ccf6236 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -101,13 +101,13 @@ const config = [ '@typescript-eslint': tseslint.plugin }, rules: { - '@typescript-eslint/no-unsafe-argument': 'warn', - '@typescript-eslint/no-unsafe-assignment': 'warn', - '@typescript-eslint/no-unsafe-call': 'warn', - '@typescript-eslint/no-unsafe-member-access': 'warn', - '@typescript-eslint/no-unsafe-return': 'warn', + '@typescript-eslint/no-unsafe-argument': 'error', + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-return': 'error', '@typescript-eslint/explicit-function-return-type': 'error', - '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-unused-vars': [ 'error', { ignoreRestSiblings: true } diff --git a/package.json b/package.json index 302762f..9b9b730 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dependencies": { "comlink": "^4.4.1", "cozy-app-publish": "^0.34.0", - "cozy-client": "^49.0.0", + "cozy-client": "^49.8.0", "cozy-device-helper": "^3.1.0", "cozy-flags": "^4.0.0", "cozy-logger": "^1.10.4", @@ -37,6 +37,7 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", "@types/jest": "^29.5.13", + "@types/pouchdb-browser": "^6.1.5", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@types/sharedworker": "^0.0.126", diff --git a/src/@types/cozy-client.d.ts b/src/@types/cozy-client.d.ts index 15f6036..bd17e10 100644 --- a/src/@types/cozy-client.d.ts +++ b/src/@types/cozy-client.d.ts @@ -170,14 +170,14 @@ declare module 'cozy-client' { } export default class CozyClient { - plugins: any + plugins: unknown constructor(rawOptions?: ClientOptions) getStackClient(): StackClient getInstanceOptions(): InstanceOptions instanceOptions: InstanceOptions collection(doctype: string): Collection isLogged: boolean - on: (event: string, callback: () => void) => void + on: (event: string, callback: (doctype: string) => void) => void removeListener: (event: string, callback: () => void) => void logout: () => Promise query: ( diff --git a/src/@types/cozy-realtime.d.ts b/src/@types/cozy-realtime.d.ts new file mode 100644 index 0000000..8d8a8b8 --- /dev/null +++ b/src/@types/cozy-realtime.d.ts @@ -0,0 +1,3 @@ +declare module 'cozy-realtime' { + export const RealtimePlugin = (): null => null +} diff --git a/src/dataproxy/worker/platformWorker.ts b/src/dataproxy/worker/platformWorker.ts index 3982b38..670ca36 100644 --- a/src/dataproxy/worker/platformWorker.ts +++ b/src/dataproxy/worker/platformWorker.ts @@ -31,7 +31,7 @@ const openDB = (): Promise => { } const storage = { - getItem: async (key: string): Promise => { + getItem: async (key: string): Promise => { const db = await openDB() return new Promise((resolve, reject) => { const transaction = db.transaction('store', 'readonly') @@ -39,6 +39,7 @@ const storage = { const request = store.get(key) request.onsuccess = (): void => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access resolve(request.result ? request.result.value : null) } @@ -48,7 +49,7 @@ const storage = { }) }, - setItem: async (key: string, value: any): Promise => { + setItem: async (key: string, value: unknown): Promise => { const db = await openDB() return new Promise((resolve, reject) => { const transaction = db.transaction('store', 'readwrite') @@ -105,6 +106,7 @@ const isOnline = async (): Promise => { export const platformWorker = { storage, events, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment pouchAdapter: PouchDB, isOnline } diff --git a/src/search/SearchEngine.ts b/src/search/SearchEngine.ts index 957e196..f42616a 100644 --- a/src/search/SearchEngine.ts +++ b/src/search/SearchEngine.ts @@ -13,7 +13,8 @@ import { LIMIT_DOCTYPE_SEARCH, REPLICATION_DEBOUNCE, ROOT_DIR_ID, - SHARED_DRIVES_DIR_ID + SHARED_DRIVES_DIR_ID, + SearchedDoctype } from '@/search/consts' import { getPouchLink } from '@/search/helpers/client' import { getSearchEncoder } from '@/search/helpers/getSearchEncoder' @@ -30,10 +31,10 @@ import { isIOCozyApp, isIOCozyContact, isIOCozyFile, - SearchedDoctype, SearchIndex, SearchIndexes, - SearchResult + SearchResult, + isSearchedDoctype } from '@/search/types' const log = Minilog('🗂️ [Indexing]') @@ -50,7 +51,7 @@ class SearchEngine { constructor(client: CozyClient) { this.client = client - this.searchIndexes = {} + this.searchIndexes = {} as SearchIndexes this.indexOnChanges() this.debouncedReplication = startReplicationWithDebounce( @@ -64,7 +65,9 @@ class SearchEngine { return } this.client.on('pouchlink:doctypesync:end', async (doctype: string) => { - await this.indexDocsForSearch(doctype) + if (isSearchedDoctype(doctype)) { + await this.indexDocsForSearch(doctype as keyof typeof SEARCH_SCHEMA) + } }) this.client.on('login', () => { // Ensure login is done before plugin register @@ -79,15 +82,17 @@ class SearchEngine { } subscribeDoctype(client: CozyClient, doctype: string): void { + /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ const realtime = this.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 */ } handleUpdatedOrCreatedDoc(doc: CozyDoc): void { - const doctype: string | undefined = doc._type - if (!doctype) { + const doctype = doc._type + if (!isSearchedDoctype(doctype)) { return } const searchIndex = this.searchIndexes?.[doctype] @@ -102,8 +107,8 @@ class SearchEngine { } handleDeletedDoc(doc: CozyDoc): void { - const doctype: string | undefined = doc._type - if (!doctype) { + const doctype = doc._type + if (!isSearchedDoctype(doctype)) { return } const searchIndex = this.searchIndexes?.[doctype] @@ -112,7 +117,7 @@ class SearchEngine { return } log.debug('[REALTIME] remove doc from index after update : ', doc) - this.searchIndexes[doctype].index.remove(doc._id) + this.searchIndexes[doctype].index.remove(doc._id!) this.debouncedReplication() } @@ -126,6 +131,7 @@ class SearchEngine { const flexsearchIndex = new FlexSearch.Document({ tokenize: 'forward', encode: getSearchEncoder(), + // @ts-ignore minlength: 2, document: { id: '_id', @@ -152,7 +158,7 @@ class SearchEngine { shouldIndexDoc(doc: CozyDoc): boolean { if (isIOCozyFile(doc)) { - const notInTrash = !doc.trashed && !/^\/\.cozy_trash/.test(doc.path) + const notInTrash = !doc.trashed && !/^\/\.cozy_trash/.test(doc.path ?? '') const notRootDir = doc._id !== ROOT_DIR_ID // Shared drives folder to be hidden in search. // The files inside it though must appear. Thus only the file with the folder ID is filtered out. @@ -163,7 +169,9 @@ class SearchEngine { return true } - async indexDocsForSearch(doctype: string): Promise { + async indexDocsForSearch( + doctype: keyof typeof SEARCH_SCHEMA + ): Promise { const searchIndex = this.searchIndexes[doctype] const pouchLink = getPouchLink(this.client) @@ -173,7 +181,9 @@ class SearchEngine { if (!searchIndex) { // First creation of search index - const docs = await this.client.queryAll(Q(doctype).limitBy(null)) + const docs = await this.client.queryAll( + Q(doctype).limitBy(null) + ) const index = this.buildSearchIndex(doctype, docs) const info = await pouchLink.getDbInfo(doctype) @@ -199,7 +209,7 @@ class SearchEngine { if (change.deleted) { searchIndex.index.remove(change.id) } else { - const normalizedDoc = { ...change.doc, _type: doctype } + const normalizedDoc = { ...change.doc, _type: doctype } as CozyDoc this.addDocToIndex(searchIndex.index, normalizedDoc) } } @@ -222,10 +232,19 @@ class SearchEngine { const appsIndex = this.buildSearchIndex('io.cozy.apps', apps) log.debug('Finished initializing indexes') + const currentDate = new Date().toISOString() this.searchIndexes = { - [FILES_DOCTYPE]: { index: filesIndex, lastSeq: 0 }, - [CONTACTS_DOCTYPE]: { index: contactsIndex, lastSeq: 0 }, - [APPS_DOCTYPE]: { index: appsIndex, lastSeq: 0 } + [FILES_DOCTYPE]: { + index: filesIndex, + lastSeq: 0, + lastUpdated: currentDate + }, + [CONTACTS_DOCTYPE]: { + index: contactsIndex, + lastSeq: 0, + lastUpdated: currentDate + }, + [APPS_DOCTYPE]: { index: appsIndex, lastSeq: 0, lastUpdated: currentDate } } return this.searchIndexes } @@ -250,7 +269,8 @@ class SearchEngine { searchOnIndexes(query: string): FlexSearchResultWithDoctype[] { let searchResults: FlexSearchResultWithDoctype[] = [] - for (const doctype in this.searchIndexes) { + for (const key in this.searchIndexes) { + const doctype = key as SearchedDoctype // XXX - Should not be necessary const index = this.searchIndexes[doctype] if (!index) { log.warn('[SEARCH] No search index available for ', doctype) @@ -258,13 +278,16 @@ class SearchEngine { } // TODO: do not use flexsearch store and rely on pouch storage? // It's better for memory, but might slow down search queries - const indexResults = index.index.search(query, { + // 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 + const indexResults = index.index.search(query, LIMIT_DOCTYPE_SEARCH, { limit: LIMIT_DOCTYPE_SEARCH, enrich: true }) const newResults = indexResults.map(res => ({ ...res, - doctype + doctype: doctype })) searchResults = searchResults.concat(newResults) } @@ -278,11 +301,14 @@ class SearchEngine { item.result.map(r => ({ ...r, field: item.field, doctype: item.doctype })) ) - const resultMap = new Map() + type MapItem = Omit<(typeof combinedResults)[number], 'field'> & { + fields: string[] + } + const resultMap = new Map() combinedResults.forEach(({ id, field, ...rest }) => { if (resultMap.has(id)) { - resultMap.get(id).fields.push(field) + resultMap.get(id)?.fields.push(field) } else { resultMap.set(id, { id, fields: [field], ...rest }) } diff --git a/src/search/consts.ts b/src/search/consts.ts index ae2e463..c081d9d 100644 --- a/src/search/consts.ts +++ b/src/search/consts.ts @@ -1,5 +1,12 @@ +const SEARCHABLE_DOCTYPES = [ + 'io.cozy.files', + 'io.cozy.contacts', + 'io.cozy.apps' +] as const +export type SearchedDoctype = (typeof SEARCHABLE_DOCTYPES)[number] + // Attribute order matters to apply priority on matching results -export const SEARCH_SCHEMA = { +export const SEARCH_SCHEMA: Record = { 'io.cozy.files': ['name', 'path'], 'io.cozy.contacts': [ 'displayName', diff --git a/src/search/helpers/replication.spec.ts b/src/search/helpers/replication.spec.ts index 570f783..f45c5e8 100644 --- a/src/search/helpers/replication.spec.ts +++ b/src/search/helpers/replication.spec.ts @@ -9,9 +9,13 @@ jest.mock('@/search/helpers/client', () => ({ getPouchLink: jest.fn() })) +interface PouchLink { + startReplication: Function +} + describe('startReplicationWithDebounce', () => { let client: CozyClient - let pouchLink: any + let pouchLink: PouchLink beforeEach(() => { client = new CozyClient() diff --git a/src/search/types.ts b/src/search/types.ts index 83fb8e5..0a89543 100644 --- a/src/search/types.ts +++ b/src/search/types.ts @@ -2,7 +2,13 @@ import FlexSearch from 'flexsearch' import { IOCozyFile, IOCozyContact, IOCozyApp } from 'cozy-client/types/types' -import { APPS_DOCTYPE, CONTACTS_DOCTYPE, FILES_DOCTYPE } from '@/search/consts' +import { + APPS_DOCTYPE, + CONTACTS_DOCTYPE, + FILES_DOCTYPE, + SEARCH_SCHEMA, + SearchedDoctype +} from '@/search/consts' export type CozyDoc = IOCozyFile | IOCozyContact | IOCozyApp @@ -18,17 +24,15 @@ export const isIOCozyApp = (doc: CozyDoc): doc is IOCozyApp => { return doc._type === APPS_DOCTYPE } -const searchedDoctypes = [ - APPS_DOCTYPE, - CONTACTS_DOCTYPE, - FILES_DOCTYPE -] as const -export type SearchedDoctype = (typeof searchedDoctypes)[number] +const searchedDoctypes = Object.keys(SEARCH_SCHEMA) export const isSearchedDoctype = ( - doctype: string + doctype: string | undefined ): doctype is SearchedDoctype => { - return true + if (!doctype) { + return false + } + return searchedDoctypes.includes(doctype) } export interface RawSearchResult @@ -52,5 +56,5 @@ export interface SearchIndex { } export type SearchIndexes = { - [key: string]: SearchIndex + [key in SearchedDoctype]: SearchIndex } diff --git a/yarn.lock b/yarn.lock index 0d35211..f4f326c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1040,6 +1040,13 @@ dependencies: "@babel/types" "^7.20.7" +"@types/debug@*": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + "@types/ejs@^3.1.5": version "3.1.5" resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.5.tgz#49d738257cc73bafe45c13cb8ff240683b4d5117" @@ -1119,6 +1126,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.10.tgz#64f3edf656af2fe59e7278b73d3e62404144a6e6" integrity sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ== +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + "@types/node@*": version "22.7.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" @@ -1126,6 +1138,69 @@ dependencies: undici-types "~6.19.2" +"@types/pouchdb-adapter-http@*": + version "6.1.6" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-http/-/pouchdb-adapter-http-6.1.6.tgz#dbb80b2c1366f114bbd7002e62999b526750ef3d" + integrity sha512-DJur1mt07GJXwGb5K+MOILoCOSgoQpsi7hybcTzRLeR3IO8Y8eq7TnhTkftAJdx9VHJGOiOXFjO+8BYM69j5yA== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-adapter-idb@*": + version "6.1.7" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-idb/-/pouchdb-adapter-idb-6.1.7.tgz#4fba402c72aa1697b7922fba6eebec131cbb8670" + integrity sha512-KwjkJ4fTNz5wPXYu20bUoWud7ty0t7tgdo4oc0AJvG+fcURAH7mI7uFmpE4dZIT+hUq5G61xu96AVq9b2q4T3g== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-adapter-websql@*": + version "6.1.7" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-websql/-/pouchdb-adapter-websql-6.1.7.tgz#774754b14385dea2dd9c41d61772e16755768c5b" + integrity sha512-9oNkP5ZCGMkQALO9KmtbHXlkBq8i2hoCEE6/gWzRicAvL1y+WIKjEQiIIEamMhj5u5tARvW3n2/r+JXwLCyYgw== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-browser@^6.1.5": + version "6.1.5" + resolved "https://registry.yarnpkg.com/@types/pouchdb-browser/-/pouchdb-browser-6.1.5.tgz#fb1d1fe9670dda05081dd690f4778b7c2e2d8eaa" + integrity sha512-f+HjxEjYFpgoYWXnMI9AQZZ+SIG8dBiBPrpfWWGsCl+48rumsP5BuBWHq/aXoB8SRKYO0XdP4TNvMBWM3UATCw== + dependencies: + "@types/pouchdb-adapter-http" "*" + "@types/pouchdb-adapter-idb" "*" + "@types/pouchdb-adapter-websql" "*" + "@types/pouchdb-core" "*" + "@types/pouchdb-mapreduce" "*" + "@types/pouchdb-replication" "*" + +"@types/pouchdb-core@*": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/pouchdb-core/-/pouchdb-core-7.0.15.tgz#02bb1b480c37b6d84b2e3c8babee716f1a101814" + integrity sha512-gq1Qbqn9nCaAKRRv6fRHZ4/ER+QYEwSXBZlDQcxwdbPrtZO8EhIn2Bct0AlguaSEdFcABfbaxxyQwFINkNQ9dQ== + dependencies: + "@types/debug" "*" + "@types/pouchdb-find" "*" + +"@types/pouchdb-find@*": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@types/pouchdb-find/-/pouchdb-find-7.3.3.tgz#599e388e2a1c4e57ee8aba0d5deca24d37b6978e" + integrity sha512-U7zXk67s9Ar+9Pwj5kSbuMnn8zif0AOOIPy4KRFeJ/S/Tk+mNS90soj+3OV21H8xyB7WTxjvS1JLablZC6C6ow== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-mapreduce@*": + version "6.1.10" + resolved "https://registry.yarnpkg.com/@types/pouchdb-mapreduce/-/pouchdb-mapreduce-6.1.10.tgz#2668c32a79920dbaed4ac335fa0fc4b986a0cb38" + integrity sha512-AgYVqCnaA5D7cWkWyzZVuk0137N4yZsmIQTD/i3DmuMxYYoFrtWUoQu0tbA52SpTRGdL8ubQ7JFQXzA13fA6IQ== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-replication@*": + version "6.4.7" + resolved "https://registry.yarnpkg.com/@types/pouchdb-replication/-/pouchdb-replication-6.4.7.tgz#09f668fa750e0c4f5fcfc01667be4b29d662f449" + integrity sha512-slB4zOwri3SAVHioFx/FWC/KqOzzb7nDFtV+qzaKzxkf+U5zTwCbK3uRHaj0d/XQk0DwVeajf1ni3Wiyq3j2OA== + dependencies: + "@types/pouchdb-core" "*" + "@types/pouchdb-find" "*" + "@types/prop-types@*": version "15.7.13" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" @@ -2173,31 +2248,6 @@ cozy-app-publish@^0.34.0: tar "^6.1.11" verror "^1.10.1" -cozy-client@^49.0.0: - version "49.0.0" - resolved "https://registry.yarnpkg.com/cozy-client/-/cozy-client-49.0.0.tgz#c38f3bdefd58cbf827a368fe126cc9e27deb996d" - integrity sha512-AY22bxwQV+46+4rSVc+zYci8pPHspolBJ1IcqvxJ+DknZrehJ5AKJ55F9DzN3aPXVNCQzmw+wpNFgVP4gShFAw== - dependencies: - "@cozy/minilog" "1.0.0" - "@types/jest" "^26.0.20" - "@types/lodash" "^4.14.170" - btoa "^1.2.1" - cozy-stack-client "^49.0.0" - date-fns "2.29.3" - json-stable-stringify "^1.0.1" - lodash "^4.17.13" - microee "^0.0.6" - node-fetch "^2.6.1" - node-polyglot "2.4.2" - open "7.4.2" - prop-types "^15.6.2" - react-redux "^7.2.0" - redux "3 || 4" - redux-thunk "^2.3.0" - server-destroy "^1.0.1" - sift "^6.0.0" - url-search-params-polyfill "^8.0.0" - cozy-client@^49.8.0: version "49.8.0" resolved "https://registry.yarnpkg.com/cozy-client/-/cozy-client-49.8.0.tgz#d4f6b3a2fb26dc6bfa561046b075eb6e221af73e" @@ -2269,15 +2319,6 @@ cozy-realtime@^5.0.2: "@cozy/minilog" "^1.0.0" cozy-device-helper "^3.1.0" -cozy-stack-client@^49.0.0: - version "49.4.0" - resolved "https://registry.yarnpkg.com/cozy-stack-client/-/cozy-stack-client-49.4.0.tgz#775d2d48e74182049977e2e423f7a42d39a4ac61" - integrity sha512-iy2vTpj25vHqErfPeclN3flI99il+WBm+Kt0FWI5YzM4H76+fE07/6Up4zOqtyOuaF7S22OHSim4Zl2hFB/SpA== - dependencies: - detect-node "^2.0.4" - mime "^2.4.0" - qs "^6.7.0" - cozy-stack-client@^49.8.0: version "49.8.0" resolved "https://registry.yarnpkg.com/cozy-stack-client/-/cozy-stack-client-49.8.0.tgz#c57dfefe50e47f228fee7e1921c438d35f4e0877"