diff --git a/manifest.webapp b/manifest.webapp index c1f370eef..f820bd394 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -88,6 +88,11 @@ "file": "services/autoDefineLabels/contacts.js", "trigger": "@event io.cozy.contacts:CREATED,UPDATED", "debounce": "5s" + }, + "cleanRelatedContacts": { + "type": "node", + "file": "services/cleanRelatedContacts/contacts.js", + "trigger": "@event io.cozy.contacts:DELETED" } } } diff --git a/src/helpers/cleanRelatedContactsService.js b/src/helpers/cleanRelatedContactsService.js new file mode 100644 index 000000000..3c79ead06 --- /dev/null +++ b/src/helpers/cleanRelatedContactsService.js @@ -0,0 +1,54 @@ +import { Q } from 'cozy-client' +import { + getHasManyItem, + removeHasManyItem +} from 'cozy-client/dist/associations/HasMany' +import logger from 'cozy-logger' + +import { DOCTYPE_CONTACTS } from '../helpers/doctypes' + +const log = logger.namespace('service/cleanRelatedContactsService') +export const cleanRelatedContactsService = async (client, contactDeletedId) => { + const allContacts = await client.queryAll( + Q(DOCTYPE_CONTACTS) + .where({ + relationships: { + related: { + data: { + $elemMatch: { + _id: contactDeletedId, + _type: DOCTYPE_CONTACTS + } + } + } + } + }) + .indexFields(['relationships.related.data']) + .limitBy(1000) + ) + log('info', `all contacts fetched, ${allContacts.length} contact(s) found`) + + if (allContacts?.length > 0) { + const contactsWithRelatedRelDeleted = allContacts.filter( + contact => + getHasManyItem(contact, 'related', contactDeletedId) !== undefined + ) + + log( + 'info', + `contact ids with related relationships deleted ${contactsWithRelatedRelDeleted + .map(contact => contact._id) + .join(', ')}` + ) + + if (contactsWithRelatedRelDeleted.length > 0) { + log('info', 'updating contacts with related contact deleted') + const allContactsUpdated = contactsWithRelatedRelDeleted.map(contact => + removeHasManyItem(contact, 'related', contactDeletedId) + ) + + log('info', 'saving all contacts updated') + await client.saveAll(allContactsUpdated) + } + } +} diff --git a/src/helpers/cleanRelatedService.spec.js b/src/helpers/cleanRelatedService.spec.js new file mode 100644 index 000000000..ba40e024d --- /dev/null +++ b/src/helpers/cleanRelatedService.spec.js @@ -0,0 +1,117 @@ +import { waitFor } from '@testing-library/react' + +import { cleanRelatedContactsService } from './cleanRelatedContactsService' + +jest.mock('cozy-client', () => ({ + Q: jest.fn(() => ({ + where: jest.fn(() => ({ + indexFields: jest.fn(() => ({ + limitBy: jest.fn() + })) + })) + })) +})) + +const setup = ({ + mockQueryAll = jest.fn(), + mockSaveAll = jest.fn(), + contacts = [] +} = {}) => { + const mockClient = { + queryAll: mockQueryAll.mockResolvedValue(contacts), + saveAll: mockSaveAll + } + + return mockClient +} + +describe('cleanRelatedContactsService', () => { + it('should update contacts with related contact deleted', async () => { + const mockSaveAll = jest.fn() + const contacts = [ + { + _id: 'contact0', + relationships: { + related: { data: [{ _id: 'contactDeletedId' }] } + } + }, + { + _id: 'contact1', + relationships: { + related: { data: [] } + } + } + ] + const client = setup({ mockSaveAll, contacts }) + + await cleanRelatedContactsService(client, 'contactDeletedId') + + await waitFor(() => { + expect(mockSaveAll).toHaveBeenCalledWith([ + { + _id: 'contact0', + relationships: { + related: { data: [] } + } + } + ]) + }) + }) + + it('should update contacts with related contact deleted and keep other relations', async () => { + const mockSaveAll = jest.fn() + const contacts = [ + { + _id: 'contact0', + relationships: { + related: { data: [{ _id: 'contactDeletedId' }, { _id: 'OtherId' }] } + } + }, + { + _id: 'contact1', + relationships: { + related: { data: [] } + } + } + ] + const client = setup({ mockSaveAll, contacts }) + + await cleanRelatedContactsService(client, 'contactDeletedId') + + await waitFor(() => { + expect(mockSaveAll).toHaveBeenCalledWith([ + { + _id: 'contact0', + relationships: { + related: { data: [{ _id: 'OtherId' }] } + } + } + ]) + }) + }) + + it('should not update contacts if no related contact deleted', async () => { + const mockSaveAll = jest.fn() + const contacts = [ + { + _id: 'contact0', + relationships: { + related: { data: [{ _id: 'otherId' }] } + } + }, + { + _id: 'contact1', + relationships: { + related: { data: [] } + } + } + ] + const client = setup({ mockSaveAll, contacts }) + + await cleanRelatedContactsService(client, 'contactDeletedId') + + await waitFor(() => { + expect(mockSaveAll).not.toHaveBeenCalled() + }) + }) +}) diff --git a/src/targets/services/cleanRelatedContacts.js b/src/targets/services/cleanRelatedContacts.js new file mode 100644 index 000000000..f9150da96 --- /dev/null +++ b/src/targets/services/cleanRelatedContacts.js @@ -0,0 +1,26 @@ +import fetch from 'node-fetch' +import { cleanRelatedContactsService } from 'src/helpers/cleanRelatedContactsService' + +import CozyClient from 'cozy-client' +import logger from 'cozy-logger' + +import { schema } from '../../helpers/doctypes' + +const log = logger.namespace('services/cleanRelatedContacts') + +global.fetch = fetch + +const cleanRelatedContacts = async () => { + log('info', 'Start cleanRelatedContacts service') + const client = CozyClient.fromEnv(process.env, { schema }) + const contactDeleted = JSON.parse(process.env.COZY_COUCH_DOC) + + await cleanRelatedContactsService(client, contactDeleted._id) + + log('info', 'All contacts successfully updated') +} + +cleanRelatedContacts().catch(e => { + log('error', e) + process.exit(1) +})