diff --git a/packages/cozy-dataproxy-lib/package.json b/packages/cozy-dataproxy-lib/package.json index aff6852c21..850d7a5e2e 100644 --- a/packages/cozy-dataproxy-lib/package.json +++ b/packages/cozy-dataproxy-lib/package.json @@ -21,13 +21,13 @@ "babel-plugin-module-resolver": "^4.0.0", "babel-plugin-tsconfig-paths": "^1.0.3", "babel-preset-cozy-app": "^2.8.1", - "cozy-client": "^60.2.0", + "cozy-client": "^60.15.2", "cozy-device-helper": "^4.0.0", "cozy-flags": "^4.8.0", "cozy-intent": "^2.30.0", "cozy-logger": "^1.17.0", "cozy-minilog": "^3.10.0", - "cozy-pouch-link": "^60.10.1", + "cozy-pouch-link": "^60.16.0", "cozy-realtime": "^5.8.0", "cross-fetch": "^4.0.0", "jest": "26.6.3", @@ -78,7 +78,7 @@ "test": "jest --config=./tests/jest.config.js", "test:watch": "yarn test --watchAll", "start": "yarn build:watch", - "lint": "cd .. && yarn eslint --ext js,jsx,ts packages/cozy-dataproxy-lib" + "lint": "cd ../.. && yarn eslint --ext js,jsx,ts packages/cozy-dataproxy-lib" }, "types": "dist/index.d.ts" } diff --git a/packages/cozy-dataproxy-lib/src/search/@types/cozy-realtime.d.ts b/packages/cozy-dataproxy-lib/src/search/@types/cozy-realtime.d.ts index d5aa1ef99a..37b6cde470 100644 --- a/packages/cozy-dataproxy-lib/src/search/@types/cozy-realtime.d.ts +++ b/packages/cozy-dataproxy-lib/src/search/@types/cozy-realtime.d.ts @@ -1,4 +1,22 @@ +import type CozyClient from 'cozy-client' + +import { CozyDoc } from '../types' + declare module 'cozy-realtime' { - export const RealtimePlugin = (): null => null - RealtimePlugin.pluginName = 'realtime' + export interface RealtimePluginType { + (): void + pluginName: 'realtime' + } + + export const RealtimePlugin: RealtimePluginType + + export default class CozyRealtime { + constructor(options: { client: CozyClient; sharedDriveId?: string }) + subscribe( + event: string, + doctype: string, + handler: (doc: CozyDoc) => void + ): void + stop(): void + } } diff --git a/packages/cozy-dataproxy-lib/src/search/SearchEngine.spec.js b/packages/cozy-dataproxy-lib/src/search/SearchEngine.spec.js index d25f79cb75..adc1f7a9e1 100644 --- a/packages/cozy-dataproxy-lib/src/search/SearchEngine.spec.js +++ b/packages/cozy-dataproxy-lib/src/search/SearchEngine.spec.js @@ -11,6 +11,25 @@ jest.mock('./helpers/client', () => ({ jest.mock('./helpers/getSearchEncoder', () => ({ getSearchEncoder: jest.fn() })) +jest.mock('./helpers/normalizeSearchResult', () => ({ + enrichResultsWithDocs: jest.fn(), + normalizeSearchResult: jest.fn() +})) +jest.mock('./storage', () => ({ + getExportDate: jest.fn(), + importSearchIndexes: jest.fn(), + exportSearchIndexes: jest.fn() +})) +jest.mock('./indexDocs', () => ({ + indexAllDocs: jest.fn(), + indexOnChanges: jest.fn(), + indexSingleDoc: jest.fn(), + initDoctypeAfterIndexImport: jest.fn(), + initSearchIndex: jest.fn() +})) +jest.mock('./queries', () => ({ + queryLocalOrRemoteDocs: jest.fn() +})) jest.mock('./consts', () => ({ LIMIT_DOCTYPE_SEARCH: 3, SEARCH_SCHEMA: { @@ -25,7 +44,19 @@ jest.mock('./consts', () => ({ }, FILES_DOCTYPE: 'io.cozy.files', CONTACTS_DOCTYPE: 'io.cozy.contacts', - APPS_DOCTYPE: 'io.cozy.apps' + APPS_DOCTYPE: 'io.cozy.apps', + SHARED_DRIVE_FILES_DOCTYPE: 'io.cozy.files.shareddrives', + SHARED_DRIVES_DIR_ID: 'io.cozy.files.shared-drives-dir', + SEARCHABLE_DOCTYPES: ['io.cozy.files', 'io.cozy.contacts', 'io.cozy.apps'] +})) + +// Mock realtime dependencies +jest.mock('cozy-realtime', () => ({ + RealtimePlugin: { + pluginName: 'realtime' + }, + __esModule: true, + default: jest.fn() })) describe('sortSearchResults', () => { @@ -218,3 +249,608 @@ describe('limitSearchResults', () => { expect(filteredResults).toEqual([]) }) }) + +describe('getSharedDrivesDoctypes', () => { + let searchEngine + let mockClient + let mockPouchLink + + beforeEach(() => { + mockClient = createMockClient() + mockPouchLink = { + doctypes: [ + 'io.cozy.files', + 'io.cozy.contacts', + 'io.cozy.apps', + 'io.cozy.files.shareddrives.drive1', + 'io.cozy.files.shareddrives.drive2' + ] + } + + const { getPouchLink } = require('./helpers/client') + getPouchLink.mockReturnValue(mockPouchLink) + + searchEngine = new SearchEngine(mockClient, {}, undefined, { + shouldInit: false + }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should return shared drives doctypes when pouch link exists', () => { + const sharedDrivesDoctypes = searchEngine.getSharedDrivesDoctypes() + + expect(sharedDrivesDoctypes).toEqual([ + 'io.cozy.files.shareddrives.drive1', + 'io.cozy.files.shareddrives.drive2' + ]) + }) + + it('should return empty array when pouch link does not exist', () => { + const { getPouchLink } = require('./helpers/client') + getPouchLink.mockReturnValue(null) + + const sharedDrivesDoctypes = searchEngine.getSharedDrivesDoctypes() + + expect(sharedDrivesDoctypes).toEqual([]) + }) + + it('should return empty array when no shared drives doctypes exist', () => { + mockPouchLink.doctypes = [ + 'io.cozy.files', + 'io.cozy.contacts', + 'io.cozy.apps' + ] + + const sharedDrivesDoctypes = searchEngine.getSharedDrivesDoctypes() + + expect(sharedDrivesDoctypes).toEqual([]) + }) +}) + +describe('search method with shared drives integration', () => { + let searchEngine + let mockClient + let mockPouchLink + let mockStorage + + beforeEach(() => { + mockClient = createMockClient() + mockStorage = { + storeData: jest.fn(), + getData: jest.fn() + } + mockPouchLink = { + doctypes: [ + 'io.cozy.files', + 'io.cozy.contacts', + 'io.cozy.apps', + 'io.cozy.files.shareddrives.drive1', + 'io.cozy.files.shareddrives.drive2' + ] + } + + const { getPouchLink } = require('./helpers/client') + getPouchLink.mockReturnValue(mockPouchLink) + + searchEngine = new SearchEngine(mockClient, mockStorage, undefined, { + shouldInit: false + }) + + // Mock search indexes with shared drives + searchEngine.searchIndexes = { + 'io.cozy.files': { index: { search: jest.fn().mockReturnValue([]) } }, + 'io.cozy.files.shareddrives.drive1': { + index: { search: jest.fn().mockReturnValue([]) } + }, + 'io.cozy.files.shareddrives.drive2': { + index: { search: jest.fn().mockReturnValue([]) } + } + } + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should include shared drives doctypes when searching files without specific doctypes', async () => { + const { + enrichResultsWithDocs, + normalizeSearchResult + } = require('./helpers/normalizeSearchResult') + enrichResultsWithDocs.mockResolvedValue([]) + normalizeSearchResult.mockReturnValue({ title: 'test', doc: {} }) + + const searchOnIndexesSpy = jest.spyOn(searchEngine, 'searchOnIndexes') + searchOnIndexesSpy.mockReturnValue([]) + + await searchEngine.search('test query', { + doctypes: [consts.FILES_DOCTYPE] + }) + + expect(searchOnIndexesSpy).toHaveBeenCalledWith('test query', [ + consts.FILES_DOCTYPE, + 'io.cozy.files.shareddrives.drive1', + 'io.cozy.files.shareddrives.drive2' + ]) + }) + + it('should include shared drives doctypes when searching without specific doctypes', async () => { + const { + enrichResultsWithDocs, + normalizeSearchResult + } = require('./helpers/normalizeSearchResult') + enrichResultsWithDocs.mockResolvedValue([]) + normalizeSearchResult.mockReturnValue({ title: 'test', doc: {} }) + + const searchOnIndexesSpy = jest.spyOn(searchEngine, 'searchOnIndexes') + searchOnIndexesSpy.mockReturnValue([]) + + await searchEngine.search('test query', {}) + + expect(searchOnIndexesSpy).toHaveBeenCalledWith('test query', [ + 'io.cozy.files.shareddrives.drive1', + 'io.cozy.files.shareddrives.drive2' + ]) + }) + + it('should not include shared drives doctypes when searching specific non-files doctypes', async () => { + const { + enrichResultsWithDocs, + normalizeSearchResult + } = require('./helpers/normalizeSearchResult') + enrichResultsWithDocs.mockResolvedValue([]) + normalizeSearchResult.mockReturnValue({ title: 'test', doc: {} }) + + const searchOnIndexesSpy = jest.spyOn(searchEngine, 'searchOnIndexes') + searchOnIndexesSpy.mockReturnValue([]) + + await searchEngine.search('test query', { + doctypes: [consts.CONTACTS_DOCTYPE] + }) + + expect(searchOnIndexesSpy).toHaveBeenCalledWith('test query', [ + consts.CONTACTS_DOCTYPE + ]) + }) + + it('should clean up non-existing doctypes from search indexes', async () => { + // Set up search indexes with doctypes that don't exist in pouch link + searchEngine.searchIndexes = { + 'io.cozy.files': { index: { search: jest.fn().mockReturnValue([]) } }, + 'io.cozy.files.shareddrives.drive1': { + index: { search: jest.fn().mockReturnValue([]) } + }, + 'non.existing.doctype': { + index: { search: jest.fn().mockReturnValue([]) } + } + } + + const { + enrichResultsWithDocs, + normalizeSearchResult + } = require('./helpers/normalizeSearchResult') + enrichResultsWithDocs.mockResolvedValue([]) + normalizeSearchResult.mockReturnValue({ title: 'test', doc: {} }) + + const searchOnIndexesSpy = jest.spyOn(searchEngine, 'searchOnIndexes') + searchOnIndexesSpy.mockReturnValue([]) + + await searchEngine.search('test query', {}) + + // Check that non-existing doctype was removed + expect(searchEngine.searchIndexes['non.existing.doctype']).toBeUndefined() + expect(searchEngine.searchIndexes['io.cozy.files']).toBeDefined() + expect( + searchEngine.searchIndexes['io.cozy.files.shareddrives.drive1'] + ).toBeDefined() + }) +}) + +describe('indexDocumentsAtInit with shared drives', () => { + let searchEngine + let mockClient + let mockStorage + let mockPouchLink + + beforeEach(() => { + mockClient = createMockClient() + mockStorage = { + storeData: jest.fn(), + getData: jest.fn().mockResolvedValue(null) // No persisted index + } + mockPouchLink = { + doctypes: [ + 'io.cozy.files', + 'io.cozy.contacts', + 'io.cozy.apps', + 'io.cozy.files.shareddrives.drive1', + 'io.cozy.files.shareddrives.drive2' + ] + } + + const { getPouchLink } = require('./helpers/client') + getPouchLink.mockReturnValue(mockPouchLink) + + searchEngine = new SearchEngine(mockClient, mockStorage, undefined, { + shouldInit: false + }) + searchEngine.isLocalSearch = true + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should index shared drives doctypes during initialization', async () => { + const { getExportDate } = require('./storage') + const { queryLocalOrRemoteDocs } = require('./queries') + const { initSearchIndex, indexAllDocs } = require('./indexDocs') + + getExportDate.mockResolvedValue(null) // No persisted index + queryLocalOrRemoteDocs.mockResolvedValue([]) + initSearchIndex.mockReturnValue({ search: jest.fn() }) + indexAllDocs.mockImplementation(() => {}) + + const indexDocsForSearchSpy = jest.spyOn(searchEngine, 'indexDocsForSearch') + indexDocsForSearchSpy.mockResolvedValue({ + index: { search: jest.fn() }, + lastSeq: 1, + lastUpdated: new Date().toISOString() + }) + + await searchEngine.indexDocumentsAtInit() + + // Should be called for standard doctypes + shared drives doctypes + expect(indexDocsForSearchSpy).toHaveBeenCalledWith('io.cozy.files') + expect(indexDocsForSearchSpy).toHaveBeenCalledWith('io.cozy.contacts') + expect(indexDocsForSearchSpy).toHaveBeenCalledWith('io.cozy.apps') + expect(indexDocsForSearchSpy).toHaveBeenCalledWith( + 'io.cozy.files.shareddrives.drive1' + ) + expect(indexDocsForSearchSpy).toHaveBeenCalledWith( + 'io.cozy.files.shareddrives.drive2' + ) + }) +}) + +// Realtime features tests +describe('Realtime features', () => { + let searchEngine + let mockClient + let mockStorage + let mockPouchLink + let mockRealtimePlugin + let mockCozyRealtime + + beforeEach(() => { + mockClient = createMockClient() + mockStorage = { + storeData: jest.fn(), + getData: jest.fn() + } + mockPouchLink = { + doctypes: ['io.cozy.files', 'io.cozy.contacts', 'io.cozy.apps'], + startReplicationWithDebounce: jest.fn(), + getDbInfo: jest.fn().mockResolvedValue({ update_seq: 1 }), + getSharedDriveDoctypes: jest.fn().mockReturnValue([]) + } + + mockRealtimePlugin = { + subscribe: jest.fn() + } + + mockCozyRealtime = jest.fn().mockImplementation(() => ({ + subscribe: jest.fn(), + stop: jest.fn() + })) + + const { getPouchLink } = require('./helpers/client') + getPouchLink.mockReturnValue(mockPouchLink) + + const CozyRealtime = require('cozy-realtime').default + CozyRealtime.mockImplementation(mockCozyRealtime) + + // Mock client plugins + mockClient.plugins = { + realtime: mockRealtimePlugin + } + mockClient.registerPlugin = jest.fn() + mockClient.on = jest.fn() + mockClient.isLogged = true + + searchEngine = new SearchEngine(mockClient, mockStorage, undefined, { + shouldInit: false + }) + searchEngine.isLocalSearch = true + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('init method - realtime setup', () => { + it('should setup shared drives realtime if pouch link has shared drives doctypes', async () => { + mockPouchLink.getSharedDriveDoctypes.mockReturnValue([ + 'io.cozy.files.shareddrives-drive1', + 'io.cozy.files.shareddrives-drive2' + ]) + + await searchEngine.init() + + expect(mockCozyRealtime).toHaveBeenCalledWith({ + client: mockClient, + sharedDriveId: 'drive1' + }) + expect(mockCozyRealtime).toHaveBeenCalledWith({ + client: mockClient, + sharedDriveId: 'drive2' + }) + }) + }) + + describe('handleUpdatedOrCreatedDoc method', () => { + beforeEach(() => { + searchEngine.searchIndexes = { + 'io.cozy.files': { + index: { search: jest.fn() } + } + } + }) + + it('should return early if doctype is not a searched doctype', () => { + const { indexSingleDoc } = require('./indexDocs') + const doc = { _type: 'io.cozy.notsearched', _id: 'test-id' } + + searchEngine.handleUpdatedOrCreatedDoc(doc) + + expect(indexSingleDoc).not.toHaveBeenCalled() + }) + + it('should return early if no search index exists for doctype', () => { + const { indexSingleDoc } = require('./indexDocs') + const doc = { _type: 'io.cozy.contacts', _id: 'test-id' } + + searchEngine.handleUpdatedOrCreatedDoc(doc) + + expect(indexSingleDoc).not.toHaveBeenCalled() + }) + + it('should call indexSingleDoc for valid doctype with existing index', () => { + const { indexSingleDoc } = require('./indexDocs') + const doc = { _type: 'io.cozy.files', _id: 'test-id' } + + searchEngine.handleUpdatedOrCreatedDoc(doc) + + expect(indexSingleDoc).toHaveBeenCalledWith( + searchEngine.searchIndexes['io.cozy.files'].index, + doc + ) + }) + + it('should trigger debounced replication for local search', () => { + const doc = { _type: 'io.cozy.files', _id: 'test-id' } + const debouncedReplicationSpy = jest.spyOn( + searchEngine, + 'debouncedReplication' + ) + + searchEngine.handleUpdatedOrCreatedDoc(doc) + + expect(debouncedReplicationSpy).toHaveBeenCalled() + }) + + it('should not trigger debounced replication for non-local search', () => { + searchEngine.isLocalSearch = false + const doc = { _type: 'io.cozy.files', _id: 'test-id' } + const debouncedReplicationSpy = jest.spyOn( + searchEngine, + 'debouncedReplication' + ) + + searchEngine.handleUpdatedOrCreatedDoc(doc) + + expect(debouncedReplicationSpy).not.toHaveBeenCalled() + }) + }) + + describe('handleDeletedDoc method', () => { + beforeEach(() => { + const mockIndex = { + remove: jest.fn() + } + searchEngine.searchIndexes = { + 'io.cozy.files': { + index: mockIndex + } + } + }) + + it('should return early if doctype is not a searched doctype', () => { + const doc = { _type: 'io.cozy.notsearched', _id: 'test-id' } + + searchEngine.handleDeletedDoc(doc) + + expect( + searchEngine.searchIndexes['io.cozy.files'].index.remove + ).not.toHaveBeenCalled() + }) + + it('should return early if no search index exists for doctype', () => { + const doc = { _type: 'io.cozy.contacts', _id: 'test-id' } + + searchEngine.handleDeletedDoc(doc) + + expect( + searchEngine.searchIndexes['io.cozy.files'].index.remove + ).not.toHaveBeenCalled() + }) + + it('should remove document from search index for valid doctype', () => { + const doc = { _type: 'io.cozy.files', _id: 'test-id' } + + searchEngine.handleDeletedDoc(doc) + + expect( + searchEngine.searchIndexes['io.cozy.files'].index.remove + ).toHaveBeenCalledWith('test-id') + }) + + it('should trigger debounced replication for local search', () => { + const doc = { _type: 'io.cozy.files', _id: 'test-id' } + const debouncedReplicationSpy = jest.spyOn( + searchEngine, + 'debouncedReplication' + ) + + searchEngine.handleDeletedDoc(doc) + + expect(debouncedReplicationSpy).toHaveBeenCalled() + }) + + it('should not trigger debounced replication for non-local search', () => { + searchEngine.isLocalSearch = false + const doc = { _type: 'io.cozy.files', _id: 'test-id' } + const debouncedReplicationSpy = jest.spyOn( + searchEngine, + 'debouncedReplication' + ) + + searchEngine.handleDeletedDoc(doc) + + expect(debouncedReplicationSpy).not.toHaveBeenCalled() + }) + }) + + describe('Shared drives realtime functionality', () => { + beforeEach(() => { + const mockRealtimeInstance = { + subscribe: jest.fn(), + stop: jest.fn() + } + mockCozyRealtime.mockReturnValue(mockRealtimeInstance) + }) + + describe('addSharedDrive method', () => { + it('should add shared drive realtime and trigger replication', () => { + const addSharedDriveRealtimeSpy = jest.spyOn( + searchEngine, + 'addSharedDriveRealtime' + ) + const debouncedReplicationSpy = jest.spyOn( + searchEngine, + 'debouncedReplication' + ) + + searchEngine.addSharedDrive('drive1') + + expect(addSharedDriveRealtimeSpy).toHaveBeenCalledWith('drive1') + expect(debouncedReplicationSpy).toHaveBeenCalled() + }) + + it('should not trigger replication for non-local search', () => { + searchEngine.isLocalSearch = false + const debouncedReplicationSpy = jest.spyOn( + searchEngine, + 'debouncedReplication' + ) + + searchEngine.addSharedDrive('drive1') + + expect(debouncedReplicationSpy).not.toHaveBeenCalled() + }) + }) + + describe('removeSharedDrive method', () => { + beforeEach(() => { + const mockRealtimeInstance = { + subscribe: jest.fn(), + stop: jest.fn() + } + searchEngine.sharedDrivesRealtimes = { + drive1: mockRealtimeInstance + } + searchEngine.searchIndexes = { + 'io.cozy.files.shareddrives-drive1': { index: { search: jest.fn() } } + } + }) + + it('should stop realtime, remove from indexes and trigger replication', () => { + const mockRealtimeInstance = searchEngine.sharedDrivesRealtimes.drive1 + const debouncedReplicationSpy = jest.spyOn( + searchEngine, + 'debouncedReplication' + ) + + searchEngine.removeSharedDrive('drive1') + + expect(mockRealtimeInstance.stop).toHaveBeenCalled() + expect(searchEngine.sharedDrivesRealtimes.drive1).toBeUndefined() + expect( + searchEngine.searchIndexes['io.cozy.files.shareddrives-drive1'] + ).toBeUndefined() + expect(debouncedReplicationSpy).toHaveBeenCalled() + }) + + it('should handle removal of non-existent drive gracefully', () => { + const debouncedReplicationSpy = jest.spyOn( + searchEngine, + 'debouncedReplication' + ) + + searchEngine.removeSharedDrive('nonexistent') + + expect(debouncedReplicationSpy).toHaveBeenCalled() + }) + + it('should not trigger replication for non-local search', () => { + searchEngine.isLocalSearch = false + const debouncedReplicationSpy = jest.spyOn( + searchEngine, + 'debouncedReplication' + ) + + searchEngine.removeSharedDrive('drive1') + + expect(debouncedReplicationSpy).not.toHaveBeenCalled() + }) + }) + + describe('addSharedDriveRealtime private method', () => { + it('should create CozyRealtime instance and subscribe to files doctype', () => { + const mockRealtimeInstance = { + subscribe: jest.fn(), + stop: jest.fn() + } + mockCozyRealtime.mockReturnValue(mockRealtimeInstance) + + searchEngine.addSharedDriveRealtime('drive1') + + expect(mockCozyRealtime).toHaveBeenCalledWith({ + client: mockClient, + sharedDriveId: 'drive1' + }) + expect(mockRealtimeInstance.subscribe).toHaveBeenCalledWith( + 'created', + consts.FILES_DOCTYPE, + expect.any(Function) + ) + expect(mockRealtimeInstance.subscribe).toHaveBeenCalledWith( + 'updated', + consts.FILES_DOCTYPE, + expect.any(Function) + ) + expect(mockRealtimeInstance.subscribe).toHaveBeenCalledWith( + 'deleted', + consts.FILES_DOCTYPE, + expect.any(Function) + ) + expect(searchEngine.sharedDrivesRealtimes.drive1).toBe( + mockRealtimeInstance + ) + }) + }) + }) +}) diff --git a/packages/cozy-dataproxy-lib/src/search/SearchEngine.ts b/packages/cozy-dataproxy-lib/src/search/SearchEngine.ts index 146c22bfdc..e00fe21b7b 100644 --- a/packages/cozy-dataproxy-lib/src/search/SearchEngine.ts +++ b/packages/cozy-dataproxy-lib/src/search/SearchEngine.ts @@ -2,9 +2,12 @@ import FlexSearch from 'flexsearch' import CozyClient, { defaultPerformanceApi } from 'cozy-client' import type { PerformanceAPI } from 'cozy-client/types/performances/types' +import { IOCozyFile } from 'cozy-client/types/types' import Minilog from 'cozy-minilog' import { RealtimePlugin } from 'cozy-realtime' +import CozyRealtime from 'cozy-realtime' +import { SHARED_DRIVE_FILES_DOCTYPE } from './consts' import { SEARCH_SCHEMA, APPS_DOCTYPE, @@ -20,7 +23,7 @@ import { enrichResultsWithDocs, normalizeSearchResult } from './helpers/normalizeSearchResult' -import { isDebug } from './helpers/utils' +import { isDebug, normalizeDoctype } from './helpers/utils' import { indexAllDocs, indexOnChanges, @@ -46,7 +49,9 @@ import { isSearchedDoctype, SearchOptions, StorageInterface, - EnrichedSearchResult + EnrichedSearchResult, + isTrashedDrive, + isInSharedDrivesDir } from './types' const log = Minilog('🗂️ [Indexing]') @@ -68,6 +73,7 @@ export class SearchEngine { storage: StorageInterface performanceApi: PerformanceAPI engineOptions: EngineOptions + sharedDrivesRealtimes: Record constructor( client: CozyClient, @@ -80,6 +86,7 @@ export class SearchEngine { this.storage = storage this.performanceApi = performanceApi ?? defaultPerformanceApi this.engineOptions = { shouldInit: true, ...engineOptions } + this.sharedDrivesRealtimes = {} this.isLocalSearch = !!getPouchLink(this.client) log.info('Use local data on trusted device: ', this.isLocalSearch) @@ -119,10 +126,11 @@ export class SearchEngine { const lastExportDate = await getExportDate(this.storage) if (!lastExportDate || !this.isLocalSearch) { // No persisted index: let's create them - for (const doctype of SEARCHABLE_DOCTYPES) { - const searchIndex = await this.indexDocsForSearch( - doctype as keyof typeof SEARCH_SCHEMA - ) + const doctypes = (SEARCHABLE_DOCTYPES as unknown as string[]).concat( + this.getSharedDrivesDoctypes() + ) + for (const doctype of doctypes) { + const searchIndex = await this.indexDocsForSearch(doctype) if (searchIndex) { this.searchIndexes[doctype] = searchIndex } @@ -148,11 +156,10 @@ export class SearchEngine { endReplicationTime = 0 this.client.on('pouchlink:doctypesync:end', async (doctype: string) => { - if (isSearchedDoctype(doctype) && this.searchIndexes[doctype]) { + const normalizedDoctype = normalizeDoctype(doctype) + if (isSearchedDoctype(normalizedDoctype)) { // Here, the index already exist, so let's have an incremental update - const searchIndex = await this.indexDocsForSearch( - doctype as keyof typeof SEARCH_SCHEMA - ) + const searchIndex = await this.indexDocsForSearch(doctype) if (searchIndex) { this.searchIndexes[doctype] = searchIndex } @@ -186,8 +193,12 @@ export class SearchEngine { async init(): Promise { // Ensure login is done before plugin register - if (!this.client.plugins[RealtimePlugin.pluginName]) { - this.client.registerPlugin(RealtimePlugin, {}) + const realtimePlugin = RealtimePlugin as unknown as { + (): void + pluginName: 'realtime' + } + if (!this.client.plugins[realtimePlugin.pluginName]) { + this.client.registerPlugin(realtimePlugin, {}) } // Realtime subscription @@ -197,6 +208,17 @@ export class SearchEngine { this.subscribeDoctype(this.client, CONTACTS_DOCTYPE) this.subscribeDoctype(this.client, APPS_DOCTYPE) + const pouchLink = getPouchLink(this.client) + if (pouchLink) { + const sharedDrivesDoctypes = pouchLink.getSharedDriveDoctypes() + for (const sharedDrivesDoctype of sharedDrivesDoctypes) { + const driveId = sharedDrivesDoctype.split('-').pop() + if (driveId) { + this.addSharedDriveRealtime(driveId) + } + } + } + if (this.isLocalSearch) { this.debouncedReplication() } @@ -204,18 +226,65 @@ export class SearchEngine { await this.indexDocumentsAtInit() } - subscribeDoctype(client: CozyClient, doctype: string): void { + addSharedDrive(driveId: string): void { + this.addSharedDriveRealtime(driveId) + if (this.isLocalSearch) { + this.debouncedReplication() + } + } + + removeSharedDrive(driveId: string): void { + this.sharedDrivesRealtimes[driveId]?.stop() + delete this.sharedDrivesRealtimes[driveId] + if ( + this.searchIndexes && + this.searchIndexes[`${SHARED_DRIVE_FILES_DOCTYPE}-${driveId}`] + ) { + delete this.searchIndexes[`${SHARED_DRIVE_FILES_DOCTYPE}-${driveId}`] + } + if (this.isLocalSearch) { + this.debouncedReplication() + } + } + + private addSharedDriveRealtime(sharedDriveId: string): void { + const realtime = new CozyRealtime({ + client: this.client, + sharedDriveId + }) + this.subscribeDoctype(this.client, FILES_DOCTYPE, realtime, sharedDriveId) + this.sharedDrivesRealtimes[sharedDriveId] = realtime + } + + subscribeDoctype( + client: CozyClient, + doctype: string, + realtime?: CozyRealtime, + sharedDriveId?: string + ): void { /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/unbound-method */ - const realtime = client.plugins.realtime - realtime.subscribe('created', doctype, this.handleUpdatedOrCreatedDoc) - realtime.subscribe('updated', doctype, this.handleUpdatedOrCreatedDoc) - realtime.subscribe('deleted', doctype, this.handleDeletedDoc) + const realtimeInstance = realtime ? realtime : client.plugins.realtime + realtimeInstance.subscribe('created', doctype, (doc: CozyDoc) => + this.handleUpdatedOrCreatedDoc(doc, sharedDriveId) + ) + realtimeInstance.subscribe('updated', doctype, (doc: CozyDoc) => + this.handleUpdatedOrCreatedDoc(doc, sharedDriveId) + ) + realtimeInstance.subscribe('deleted', doctype, (doc: CozyDoc) => + this.handleDeletedDoc(doc, sharedDriveId) + ) /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/unbound-method */ } - handleUpdatedOrCreatedDoc(doc: CozyDoc): void { - const doctype = doc._type - if (!isSearchedDoctype(doctype)) { + handleUpdatedOrCreatedDoc(doc: CozyDoc, sharedDriveId?: string): void { + const doctype = sharedDriveId + ? `${SHARED_DRIVE_FILES_DOCTYPE}-${sharedDriveId}` + : doc._type + if ( + !doctype || + !isSearchedDoctype(normalizeDoctype(doctype)) || + isTrashedDrive(doc as IOCozyFile, sharedDriveId) + ) { return } const searchIndex = this.searchIndexes?.[doctype] @@ -230,9 +299,15 @@ export class SearchEngine { indexSingleDoc(searchIndex.index, doc) } - handleDeletedDoc(doc: CozyDoc): void { - const doctype = doc._type - if (!isSearchedDoctype(doctype)) { + handleDeletedDoc(doc: CozyDoc, sharedDriveId?: string): void { + const doctype = sharedDriveId + ? `${SHARED_DRIVE_FILES_DOCTYPE}-${sharedDriveId}` + : doc._type + if ( + !doctype || + !isSearchedDoctype(normalizeDoctype(doctype)) || + isInSharedDrivesDir(doc as IOCozyFile) + ) { return } const searchIndex = this.searchIndexes?.[doctype] @@ -283,7 +358,7 @@ export class SearchEngine { } buildSearchIndex( - doctype: keyof typeof SEARCH_SCHEMA, + doctype: string, docs: CozyDoc[] ): FlexSearch.Document { const startTimeIndex = performance.now() @@ -300,7 +375,7 @@ export class SearchEngine { return flexsearchIndex } - async getLocalLastSeq(doctype: keyof typeof SEARCH_SCHEMA): Promise { + async getLocalLastSeq(doctype: string): Promise { if (this.isLocalSearch) { const pouchLink = getPouchLink(this.client) const info = pouchLink ? await pouchLink.getDbInfo(doctype) : null @@ -309,16 +384,10 @@ export class SearchEngine { return -1 } - async initialIndexation( - doctype: keyof typeof SEARCH_SCHEMA - ): Promise { + async initialIndexation(doctype: string): Promise { const docs = await queryLocalOrRemoteDocs(this.client, doctype, { isLocalSearch: this.isLocalSearch }) - if (docs.length < 1) { - // No docs available yet - return null - } const index = this.buildSearchIndex(doctype, docs) const lastSeq = await this.getLocalLastSeq(doctype) @@ -331,15 +400,15 @@ export class SearchEngine { } async incrementalIndexation( - doctype: keyof typeof SEARCH_SCHEMA, + doctype: string, searchIndex: SearchIndex ): Promise { - return indexOnChanges(this, searchIndex, doctype) + const updatedSearchIndex = await indexOnChanges(this, searchIndex, doctype) + + return updatedSearchIndex } - async indexDocsForSearch( - doctype: keyof typeof SEARCH_SCHEMA - ): Promise { + async indexDocsForSearch(doctype: string): Promise { const markeNameIndex = this.performanceApi.mark( `indexDocsForSearch ${doctype}` ) @@ -393,9 +462,24 @@ export class SearchEngine { return null } + const pouchLink = getPouchLink(this.client) + const currentDoctypes = pouchLink?.doctypes || [] + this.cleanIndexes(currentDoctypes) + + const optionsDoctypes = options?.doctypes || [] + if ( + optionsDoctypes.includes(FILES_DOCTYPE) || + optionsDoctypes.length === 0 + ) { + const sharedDrivesDoctypes = Object.keys(this.searchIndexes).filter( + doctype => doctype.includes(SHARED_DRIVE_FILES_DOCTYPE) + ) + optionsDoctypes.push(...sharedDrivesDoctypes) + } + const markeNameIndex = this.performanceApi.mark('search') - const allResults = this.searchOnIndexes(query, options?.doctypes) + const allResults = this.searchOnIndexes(query, optionsDoctypes) const dedupResults = this.deduplicateAndFlatten(allResults) const enrichedResults = await enrichResultsWithDocs( this.client, @@ -403,7 +487,7 @@ export class SearchEngine { ) const sortedResults = this.sortSearchResults( enrichedResults, - options?.doctypes + optionsDoctypes ) const results = this.limitSearchResults(sortedResults) @@ -423,14 +507,16 @@ export class SearchEngine { searchOnIndexes( query: string, - searchOnDoctypes: string[] | undefined + searchOnDoctypes: string[] ): FlexSearchResultWithDoctype[] { let searchResults: FlexSearchResultWithDoctype[] = [] for (const key in this.searchIndexes) { const doctype = key as SearchedDoctype // XXX - Should not be necessary + const isSearchOnDoctypesDefined = searchOnDoctypes.length > 0 + if ( searchOnDoctypes && - searchOnDoctypes?.length > 0 && + isSearchOnDoctypesDefined && !searchOnDoctypes.includes(doctype) ) { // Search only on specified doctypes @@ -587,4 +673,29 @@ export class SearchEngine { return doctypesCount[doctype] <= LIMIT_DOCTYPE_SEARCH }) } + + getSharedDrivesDoctypes(): string[] { + const pouchLink = getPouchLink(this.client) + if (!pouchLink) { + return [] + } + return pouchLink.doctypes.filter(dtype => + dtype.includes(SHARED_DRIVE_FILES_DOCTYPE) + ) + } + + /** + * Clean up search indexes for doctypes that no longer exist + * @param currentDoctypes - List of currently valid doctypes + */ + private cleanIndexes(currentDoctypes: string[]): void { + const existingDoctypes = Object.keys(this.searchIndexes) + const nonExistingDoctypes = existingDoctypes.filter( + doctype => !currentDoctypes.includes(doctype) + ) + for (const doctype of nonExistingDoctypes) { + delete this.searchIndexes[doctype] + log.debug('[SEARCH] Delete index for non-existing doctype', doctype) + } + } } diff --git a/packages/cozy-dataproxy-lib/src/search/consts.ts b/packages/cozy-dataproxy-lib/src/search/consts.ts index 6c6ac26a5a..c45c35c256 100644 --- a/packages/cozy-dataproxy-lib/src/search/consts.ts +++ b/packages/cozy-dataproxy-lib/src/search/consts.ts @@ -25,6 +25,7 @@ export const SEARCH_SCHEMA: Record = { export const FILES_DOCTYPE = 'io.cozy.files' export const CONTACTS_DOCTYPE = 'io.cozy.contacts' export const APPS_DOCTYPE = 'io.cozy.apps' +export const SHARED_DRIVE_FILES_DOCTYPE = 'io.cozy.files.shareddrives' export const TYPE_DIRECTORY = 'directory' export const TYPE_FILE = 'file' diff --git a/packages/cozy-dataproxy-lib/src/search/helpers/normalizeSearchResult.spec.ts b/packages/cozy-dataproxy-lib/src/search/helpers/normalizeSearchResult.spec.ts index 18064d0ebf..0ad81de74b 100644 --- a/packages/cozy-dataproxy-lib/src/search/helpers/normalizeSearchResult.spec.ts +++ b/packages/cozy-dataproxy-lib/src/search/helpers/normalizeSearchResult.spec.ts @@ -48,6 +48,38 @@ describe('Should normalize files results', () => { }) }) + test('Should handle shared drive files', () => { + const doc = { + _id: 'SHARED_FILE_ID', + // No _type on purpose, shared-drive files might not have FILES_DOCTYPE + type: 'file', + dir_id: 'PARENT_ID', + name: 'SHARED_FILE_NAME', + path: 'SHARED/FILE/PATH', + driveId: 'DRIVE_ID' + } + const searchResult = { + doctype: 'io.cozy.files', + doc: doc + } as unknown as EnrichedSearchResult + + const result = normalizeSearchResult( + fakeFlatDomainClient, + searchResult, + 'someQuery' + ) + + expect(result).toStrictEqual({ + doc: doc, + slug: 'drive', + title: 'SHARED_FILE_NAME', + subTitle: 'SHARED/FILE/PATH', + url: 'https://claude-drive.mycozy.cloud/#/shareddrive/DRIVE_ID/PARENT_ID/file/SHARED_FILE_ID', + secondaryUrl: + 'https://claude-drive.mycozy.cloud/#/shareddrive/DRIVE_ID/PARENT_ID' + }) + }) + test('Should handle notes', () => { const doc = { _id: 'SOME_NOTE_ID', diff --git a/packages/cozy-dataproxy-lib/src/search/helpers/normalizeSearchResult.ts b/packages/cozy-dataproxy-lib/src/search/helpers/normalizeSearchResult.ts index cb5d78406c..45729a7221 100644 --- a/packages/cozy-dataproxy-lib/src/search/helpers/normalizeSearchResult.ts +++ b/packages/cozy-dataproxy-lib/src/search/helpers/normalizeSearchResult.ts @@ -2,7 +2,7 @@ import CozyClient, { generateWebLink, models } from 'cozy-client' import { IOCozyContact } from 'cozy-client/types/types' import Minilog from 'cozy-minilog' -import { APPS_DOCTYPE, TYPE_DIRECTORY } from '../consts' +import { APPS_DOCTYPE, TYPE_DIRECTORY, TYPE_FILE } from '../consts' import { queryDocsByIds } from '../queries' import { CozyDoc, @@ -10,6 +10,7 @@ import { isIOCozyApp, isIOCozyContact, isIOCozyFile, + isIOCozySharedDriveFile, SearchResult, EnrichedSearchResult } from '../types' @@ -195,6 +196,12 @@ const buildOpenURL = ( } else { urlHash = `${folderURLHash}/file/${doc._id}` } + if (isIOCozySharedDriveFile(doc)) { + urlHash = `/shareddrive/${doc.driveId}/${dirId}` + if (doc.type === TYPE_FILE) { + urlHash += `/file/${doc._id}` + } + } } if (isIOCozyContact(doc)) { @@ -224,7 +231,11 @@ const buildSecondaryURL = ( return null } - const folderURLHash = `/folder/${doc.dir_id}` + let folderURLHash = `/folder/${doc.dir_id}` + + if (isIOCozySharedDriveFile(doc)) { + folderURLHash = `/shareddrive/${doc.driveId}/${doc.dir_id}` // FIXME this url hash for shared drives should be in cozy-client + } return generateWebLink({ cozyUrl: client.getStackClient().uri, diff --git a/packages/cozy-dataproxy-lib/src/search/helpers/utils.ts b/packages/cozy-dataproxy-lib/src/search/helpers/utils.ts index b9b89f4f70..f537c4fa3c 100644 --- a/packages/cozy-dataproxy-lib/src/search/helpers/utils.ts +++ b/packages/cozy-dataproxy-lib/src/search/helpers/utils.ts @@ -1,5 +1,26 @@ import flag from 'cozy-flags' +import { + FILES_DOCTYPE, + SHARED_DRIVE_FILES_DOCTYPE, + SearchedDoctype +} from '../consts' +import { isSearchedDoctype } from '../types' + export const isDebug = (): boolean => { return Boolean(flag('debug')) } + +export const normalizeDoctype = ( + doctype: string | undefined +): SearchedDoctype => { + if (doctype && doctype.includes(SHARED_DRIVE_FILES_DOCTYPE)) { + return FILES_DOCTYPE as SearchedDoctype + } + // Only return the doctype if it's a valid SearchedDoctype + if (isSearchedDoctype(doctype)) { + return doctype as SearchedDoctype + } + // Fallback to FILES_DOCTYPE for unknown doctypes + return FILES_DOCTYPE as SearchedDoctype +} diff --git a/packages/cozy-dataproxy-lib/src/search/indexDocs.ts b/packages/cozy-dataproxy-lib/src/search/indexDocs.ts index 63cfa86904..88116f0eda 100644 --- a/packages/cozy-dataproxy-lib/src/search/indexDocs.ts +++ b/packages/cozy-dataproxy-lib/src/search/indexDocs.ts @@ -8,13 +8,14 @@ import { getPouchLink } from './helpers/client' import { getSearchEncoder } from './helpers/getSearchEncoder' import { shouldKeepApp } from './helpers/normalizeApp' import { shouldKeepFile } from './helpers/normalizeFile' +import { normalizeDoctype } from './helpers/utils' import { queryLocalOrRemoteDocs } from './queries' import { CozyDoc, isIOCozyFile, isIOCozyApp, SearchIndex } from './types' export const initSearchIndex = ( - doctype: keyof typeof SEARCH_SCHEMA + doctype: string ): FlexSearch.Document => { - const fieldsToIndex = SEARCH_SCHEMA[doctype] + const fieldsToIndex = SEARCH_SCHEMA[normalizeDoctype(doctype)] const flexsearchIndex = new FlexSearch.Document({ tokenize: 'reverse', // See https://github.com/nextapps-de/flexsearch?tab=readme-ov-file#tokenizer diff --git a/packages/cozy-dataproxy-lib/src/search/queries/index.ts b/packages/cozy-dataproxy-lib/src/search/queries/index.ts index 7fdf970530..bf26f9d683 100644 --- a/packages/cozy-dataproxy-lib/src/search/queries/index.ts +++ b/packages/cozy-dataproxy-lib/src/search/queries/index.ts @@ -3,12 +3,13 @@ import { IOCozyFile } from 'cozy-client/types/types' import Minilog from 'cozy-minilog' import { FILES_DOCTYPE, TYPE_DIRECTORY } from '../consts' +import { getPouchLink } from '../helpers/client' import { normalizeFileWithFolders, shouldKeepFile } from '../helpers/normalizeFile' -import { CozyDoc } from '../types' import { isDebug } from '../helpers/utils' +import { CozyDoc } from '../types' const log = Minilog('🗂️ [Indexing]') @@ -51,7 +52,7 @@ export const queryFilesForSearch = async ( return normalizedFiles } -export const queryAllDocs = async ( +export const queryAllDocs = ( client: CozyClient, doctype: string ): Promise => { @@ -59,7 +60,18 @@ export const queryAllDocs = async ( as: `${doctype}/all`, fetchPolicies: defaultFetchPolicy } - return client.queryAll(Q(doctype).limitBy(null), queryOpts) + const pouchLink = getPouchLink(client) + let isSharedDriveDoctype = false + if (pouchLink) { + isSharedDriveDoctype = pouchLink + .getSharedDriveDoctypes() + .includes(doctype) as boolean + } + if (isSharedDriveDoctype) { + return querySharedDriveFiles(client, doctype) + } else { + return client.queryAll(Q(doctype).limitBy(null), queryOpts) + } } export const queryDocById = async ( @@ -99,6 +111,33 @@ export const queryDocsByIds = async ( return resp.data } +export const querySharedDriveFiles = async ( + client: CozyClient, + doctype: string +): Promise => { + const pouchLink = getPouchLink(client) + + if (!pouchLink) { + return [] + } + + const pouch = pouchLink.getPouch(doctype) as unknown as { + allDocs: (opts: { + include_docs: boolean + }) => Promise> + } + interface PouchAllDocsDocRow { + doc: T + } + interface PouchAllDocsResponse { + rows: Array> + } + const resp: PouchAllDocsResponse = await pouch.allDocs({ + include_docs: true + }) + return resp.rows.map(row => ({ ...row.doc, _type: doctype } as CozyDoc)) +} + export const queryLocalOrRemoteDocs = async ( client: CozyClient, doctype: string, diff --git a/packages/cozy-dataproxy-lib/src/search/types.ts b/packages/cozy-dataproxy-lib/src/search/types.ts index b9bfdf0266..444d65e494 100644 --- a/packages/cozy-dataproxy-lib/src/search/types.ts +++ b/packages/cozy-dataproxy-lib/src/search/types.ts @@ -7,13 +7,18 @@ import { CONTACTS_DOCTYPE, FILES_DOCTYPE, SEARCH_SCHEMA, - SearchedDoctype + SearchedDoctype, + SHARED_DRIVES_DIR_ID } from './consts' export type CozyDoc = IOCozyFile | IOCozyContact | IOCozyApp export const isIOCozyFile = (doc: CozyDoc): doc is IOCozyFile => { - return doc._type === FILES_DOCTYPE + return doc._type === FILES_DOCTYPE || doc.driveId !== undefined // FIXME find a way to add the right doctype in the result +} + +export const isIOCozySharedDriveFile = (doc: CozyDoc): doc is IOCozyFile => { + return doc.driveId !== undefined } export const isIOCozyContact = (doc: CozyDoc): doc is IOCozyContact => { @@ -35,6 +40,20 @@ export const isSearchedDoctype = ( return searchedDoctypes.includes(doctype) } +export const isTrashedDrive = ( + doc: IOCozyFile, + sharedDriveId: string | undefined +): boolean => { + return ( + !!sharedDriveId && + (doc as IOCozyFile & { restore_path?: string }).restore_path === '/Drives' + ) +} + +export const isInSharedDrivesDir = (doc: IOCozyFile): boolean => { + return doc.dir_id === SHARED_DRIVES_DIR_ID +} + export interface SearchOptions { doctypes: string[] // Specify which doctypes should be searched, and their order } @@ -65,7 +84,7 @@ export interface SearchIndex { } export type SearchIndexes = { - [key in SearchedDoctype]: SearchIndex + [key: string]: SearchIndex } export interface StorageInterface { diff --git a/packages/cozy-dataproxy-lib/tests/jest.config.js b/packages/cozy-dataproxy-lib/tests/jest.config.js index c152c12deb..77971e4e01 100644 --- a/packages/cozy-dataproxy-lib/tests/jest.config.js +++ b/packages/cozy-dataproxy-lib/tests/jest.config.js @@ -12,6 +12,7 @@ const config = { './src/search/helpers/getSearchEncoder.ts' ], transformIgnorePatterns: ['node_modules/(?!(flexsearch)/)'], + testPathIgnorePatterns: ['/node_modules/', '/dist/'], rootDir: '../', testMatch: ['./**/*.spec.{ts,tsx,js}'], coverageThreshold: { diff --git a/yarn.lock b/yarn.lock index fdbaaf0961..2e685ae4ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16663,17 +16663,17 @@ __metadata: languageName: node linkType: hard -"cozy-client@npm:^60.2.0": - version: 60.3.0 - resolution: "cozy-client@npm:60.3.0" +"cozy-client@npm:^60.15.2": + version: 60.15.2 + resolution: "cozy-client@npm:60.15.2" dependencies: "@cozy/minilog": "npm:1.0.0" "@fastify/deepmerge": "npm:^2.0.2" "@types/jest": "npm:^26.0.20" "@types/lodash": "npm:^4.14.170" btoa: "npm:^1.2.1" - cozy-stack-client: "npm:^60.1.0" - date-fns: "npm:2.29.3" + cozy-stack-client: "npm:^60.15.1" + date-fns: "npm:^2.30.0" fast-deep-equal: "npm:^3.1.3" json-stable-stringify: "npm:^1.0.1" lodash: "npm:^4.17.13" @@ -16698,7 +16698,7 @@ __metadata: react-native-google-play-integrity: ^1.1.0 react-native-inappbrowser-reborn: ^3.5.1 react-native-ios11-devicecheck: ^0.0.3 - checksum: 10c0/77fe471ecf767cdc8041e59fd3de1566c3d8c9cf8e1a7e8c97e8502c62138cc0bffb88d5f75f2dc6d84076e1d5296254db5c6f1b3c5249158a7b0f1d940557a3 + checksum: 10c0/c3a94bae7b9baef943d0824c564cb5c5132821d91556fae93eafd636db463f44423aa57121f0d8ea2d3fb7e2174fc5f4b5002473301a07e167cc6748213b63c8 languageName: node linkType: hard @@ -16741,45 +16741,6 @@ __metadata: languageName: node linkType: hard -"cozy-client@npm:^60.9.0": - version: 60.9.0 - resolution: "cozy-client@npm:60.9.0" - dependencies: - "@cozy/minilog": "npm:1.0.0" - "@fastify/deepmerge": "npm:^2.0.2" - "@types/jest": "npm:^26.0.20" - "@types/lodash": "npm:^4.14.170" - btoa: "npm:^1.2.1" - cozy-stack-client: "npm:^60.6.0" - date-fns: "npm:^2.30.0" - fast-deep-equal: "npm:^3.1.3" - json-stable-stringify: "npm:^1.0.1" - lodash: "npm:^4.17.13" - microee: "npm:^0.0.6" - node-fetch: "npm:^2.6.1" - node-polyglot: "npm:2.4.2" - open: "npm:7.4.2" - prop-types: "npm:^15.6.2" - react-redux: "npm:^7.2.0" - redux: "npm:3 || 4" - redux-thunk: "npm:^2.3.0" - server-destroy: "npm:^1.0.1" - sift: "npm:^6.0.0" - url-search-params-polyfill: "npm:^8.0.0" - peerDependencies: - cozy-device-helper: ">=2.1.0" - cozy-flags: ">2.8.6" - cozy-intent: ">=2.23.0" - cozy-logger: ">1.7.0" - react: ^16.7.0 - react-native: ~0.63.5 - react-native-google-play-integrity: ^1.1.0 - react-native-inappbrowser-reborn: ^3.5.1 - react-native-ios11-devicecheck: ^0.0.3 - checksum: 10c0/7be0c5d2be7aa47c88c363be1ac8757b63d26334ab970fc33b29170ccc1b036444b9cb543fac87530f149c9c9eac04301da34530bb1285d6e564799c5da643c7 - languageName: node - linkType: hard - "cozy-dataproxy-lib@workspace:packages/cozy-dataproxy-lib": version: 0.0.0-use.local resolution: "cozy-dataproxy-lib@workspace:packages/cozy-dataproxy-lib" @@ -16792,13 +16753,13 @@ __metadata: babel-plugin-tsconfig-paths: "npm:^1.0.3" babel-preset-cozy-app: "npm:^2.8.1" comlink: "npm:4.4.1" - cozy-client: "npm:^60.2.0" + cozy-client: "npm:^60.15.2" cozy-device-helper: "npm:^4.0.0" cozy-flags: "npm:^4.8.0" cozy-intent: "npm:^2.30.0" cozy-logger: "npm:^1.17.0" cozy-minilog: "npm:^3.10.0" - cozy-pouch-link: "npm:^60.10.1" + cozy-pouch-link: "npm:^60.16.0" cozy-realtime: "npm:^5.8.0" cross-fetch: "npm:^4.0.0" flexsearch: "npm:0.7.43" @@ -17299,18 +17260,18 @@ __metadata: languageName: node linkType: hard -"cozy-pouch-link@npm:^60.10.1": - version: 60.10.1 - resolution: "cozy-pouch-link@npm:60.10.1" +"cozy-pouch-link@npm:^60.16.0": + version: 60.16.0 + resolution: "cozy-pouch-link@npm:60.16.0" dependencies: - cozy-client: "npm:^60.9.0" + cozy-client: "npm:^60.15.2" pouchdb-browser: "npm:^7.2.2" pouchdb-find: "npm:^7.2.2" peerDependencies: "@cozy/minilog": 1.0.0 "@op-engineering/op-sqlite": "*" cozy-device-helper: ">=2.1.0" - checksum: 10c0/c5de3851fdea1e0f184a7d770d59a1bdbee3db79e8217814f0fadee79bc6bcb5bccc75a37562c87c46b9268fc79b62676ec4266e3ef79cb01254eeaf8a52ff7e + checksum: 10c0/e9bad772c0ff561335154e67d2285a2f1dfa5f3c73599f85ea76e3702c9d094ee9836133d528c7073376a4c9ee4e01ccf579bee7d74225d7a182c567a1dbe365 languageName: node linkType: hard @@ -17587,16 +17548,16 @@ __metadata: languageName: node linkType: hard -"cozy-stack-client@npm:^60.6.0": - version: 60.6.0 - resolution: "cozy-stack-client@npm:60.6.0" +"cozy-stack-client@npm:^60.15.1": + version: 60.15.1 + resolution: "cozy-stack-client@npm:60.15.1" dependencies: detect-node: "npm:^2.0.4" mime: "npm:^2.4.0" qs: "npm:^6.7.0" peerDependencies: cozy-flags: ">2.8.6" - checksum: 10c0/64cbc1d602866e3a1667eb0ab3cbf8d3448d86aa5eaeb55235eb3c6b3ec434f64ee54682a2d2b850f42df8720fac2a69ea8715afffba2aa3f53cc9c830290c03 + checksum: 10c0/f11e8e2a0ebd3794b4b1d078a28e3a91e2ddf13925f0f7212f21d390ca6fa20129bf33422442702f27507a87de5eb8c1b919a081b0d43b4c886c898b84b94381 languageName: node linkType: hard