Skip to content

Commit

Permalink
feat: Synchronize relation between contacts
Browse files Browse the repository at this point in the history
Following on from the work started on this PR #985.

The aim is to synchronize relationship changes
between the contacts concerned.
  • Loading branch information
Merkur39 committed Nov 15, 2024
1 parent b1e798d commit 0e679b7
Show file tree
Hide file tree
Showing 6 changed files with 526 additions and 8 deletions.
2 changes: 1 addition & 1 deletion src/components/Modals/ContactFormModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const ContactFormModal = () => {
try {
await createOrUpdateContact({
client,
isUpdated: !!currentContact,
oldContact: currentContact,
formData,
selectedGroup
})
Expand Down
10 changes: 8 additions & 2 deletions src/components/Modals/ContactFormModal.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ describe('ContactFormModal component', () => {

expect(createOrUpdateContact).toBeCalledWith({
client: expect.anything(),
isUpdated: false,
oldContact: undefined,
formData: expected,
selectedGroup: expect.anything()
})
Expand Down Expand Up @@ -224,7 +224,13 @@ describe('ContactFormModal component', () => {

expect(createOrUpdateContact).toHaveBeenCalledWith({
client: expect.anything(),
isUpdated: true,
oldContact: {
_id: 'ID',
name: {
familyName: 'John',
givenName: 'Doe'
}
},
formData: expected,
selectedGroup: expect.anything()
})
Expand Down
36 changes: 31 additions & 5 deletions src/connections/allContacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import uniqWith from 'lodash/uniqWith'

import minilog from 'cozy-minilog'

import {
getRelatedRelationshipsToUpdate,
updateRelatedRelationships
} from './relatedRelationships'
import { addGroupToContact } from '../helpers/contacts'
import { DOCTYPE_CONTACTS } from '../helpers/doctypes'
import { hasSelectedGroup } from '../helpers/groups'
Expand Down Expand Up @@ -77,14 +81,14 @@ export const deleteTrashedContacts = async client => {
/**
* @param {Object} params
* @param {import('cozy-client/types/CozyClient').default} params.client - CozyClient
* @param {boolean} params.isUpdated - Is the contact updated
* @param {import('cozy-client/types/types').ContactDocument} params.oldContact - Is the contact updated
* @param {Object} params.formData - Contact data
* @param {Object} params.selectedGroup - Selected group
* @returns {Promise<import('cozy-client/types/types').IOCozyContact>} - Contact
*/
export const createOrUpdateContact = async ({
client,
isUpdated,
oldContact,
formData,
selectedGroup
}) => {
Expand All @@ -93,9 +97,31 @@ export const createOrUpdateContact = async ({
if (hasSelectedGroup(selectedGroup)) {
updatedContact = addGroupToContact(updatedContact, selectedGroup)
}
return isUpdated
? client.save(updatedContact)
: client.create(DOCTYPE_CONTACTS, updatedContact)

const relatedRelationships = getRelatedRelationshipsToUpdate(
oldContact,
updatedContact
)

const { data: originalContact } = oldContact
? await client.save(updatedContact)
: await client.create(DOCTYPE_CONTACTS, updatedContact)

if (relatedRelationships) {
try {
const relatedContactsToSaved = await updateRelatedRelationships({
client,
relatedRelationships,
originalContactId: originalContact._id
})

client.saveAll(relatedContactsToSaved)
} catch (error) {
log.error('Error updating related contacts', error)
}
}

return originalContact
}

/**
Expand Down
263 changes: 263 additions & 0 deletions src/connections/relatedRelationships.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import difference from 'lodash/difference'
import isEqual from 'lodash/isEqual'

import {
getHasManyItems,
setHasManyItem,
removeHasManyItem
} from 'cozy-client/dist/associations/HasMany'
import minilog from 'cozy-minilog'

import { DOCTYPE_CONTACTS } from '../helpers/doctypes'
import { buildContactsQueryById } from '../queries/queries'

const log = minilog('connections/relatedRelationships')

const RELATED_RELATION_TYPES_MAPPING = {
parent: 'child',
child: 'parent',
sibling: 'sibling',
spouse: 'spouse',
coResident: 'coResident',
friend: 'friend',
colleague: 'colleague',
coWorker: 'coWorker',
acquaintance: 'acquaintance',
helper: 'recipient',
recipient: 'helper',
related: 'related' // default value
}

/**
* @param {string[]} relationTypes - Relation types
*/
const getRelationTypesMapping = relationTypes => {
return relationTypes.map(relationType => {
return RELATED_RELATION_TYPES_MAPPING[relationType]
})
}

/**
* @param {import('cozy-client/types/types').IOCozyContact} oldContact - Old contact
* @param {import('cozy-client/types/types').IOCozyContact} updatedContact - Updated contact
* @returns {boolean} - True if related relationships are the same
*/
export const isSameRelatedRelationships = (oldContact, updatedContact) => {
return isEqual(
getHasManyItems(oldContact, 'related'),
getHasManyItems(updatedContact, 'related')
)
}

/**
* @param {import('cozy-client/types/types').IOCozyContact} oldContact - Old contact
* @param {import('cozy-client/types/types').IOCozyContact} updatedContact - Updated contact
* @returns {import('../types').RelatedRelationshipsToUpdate[]|null} - Related relationships to update
*/
export const getRelatedRelationshipsToUpdate = (oldContact, updatedContact) => {
const isSameRelatedContactRelationships = isSameRelatedRelationships(
oldContact,
updatedContact
)
if (isSameRelatedContactRelationships) {
return null
}

const newRelatedRel = getHasManyItems(updatedContact, 'related')
const oldRelatedRel = getHasManyItems(oldContact, 'related')
const relatedRelToUpdate = []

// Find updated or deleted related contacts
oldRelatedRel.forEach(oldRel => {
const newRel = newRelatedRel.find(newRel => newRel._id === oldRel._id)

if (!newRel) {
relatedRelToUpdate.push({
type: 'DELETE',
relation: oldRel
})
} else {
const oldRelationTypes = oldRel.metadata.relationTypes
const newRelationTypes = newRel.metadata.relationTypes

const addedRelationTypes = difference(newRelationTypes, oldRelationTypes)
const removedRelationTypes = difference(
oldRelationTypes,
newRelationTypes
)

if (addedRelationTypes.length > 0 || removedRelationTypes.length > 0) {
relatedRelToUpdate.push({
type: 'UPDATE',
relation: newRel
})
}
}
})

// Find new related contacts
newRelatedRel.forEach(newRel => {
const oldRel = oldRelatedRel.find(oldRel => oldRel._id === newRel._id)
if (!oldRel) {
relatedRelToUpdate.push({
type: 'CREATE',
relation: newRel
})
}
})

if (relatedRelToUpdate.length === 0) {
return null
}

return relatedRelToUpdate
}

/**
* Make the new related object converted for the related contact
* @param {import('cozy-client/types/types').IOCozyContact} originalContactRelation - Original contact relation
* @param {string} originalContactId - Original contact id
*/
export const makeRelationMapping = (
originalContactRelation,
originalContactId
) => {
const relationTypesMapping = getRelationTypesMapping(
originalContactRelation.metadata.relationTypes
)
return {
_id: originalContactId,
_type: DOCTYPE_CONTACTS,
metadata: {
relationTypes: relationTypesMapping
}
}
}

/**
* @param {Object} params
* @param {import('cozy-client/types/CozyClient').default} params.client - CozyClient
* @param {import('../types').RelatedRelationships} params.relation - Related relationships to update
* @param {string} params.originalContactId - Original contact id
* @returns {Promise<import('cozy-client/types/types').IOCozyContact[]>} - Related contacts
*/
export const createRelatedContact = async ({
client,
relation,
originalContactId
}) => {
try {
const contactsQueryById = buildContactsQueryById(relation._id)
const { data: relatedContact } = await client.query(
contactsQueryById.definition()
)
const relationConverted = makeRelationMapping(relation, originalContactId)
const updatedRelatedContact = setHasManyItem(
relatedContact,
'related',
originalContactId,
relationConverted
)

return updatedRelatedContact
} catch (error) {
log.error('Error creating related contact', error)
}
}

/**
* @param {Object} params
* @param {import('cozy-client/types/CozyClient').default} params.client - CozyClient
* @param {import('../types').RelatedRelationships} params.relation - Related relationships to update
* @param {string} params.originalContactId - Original contact id
* @returns {Promise<import('cozy-client/types/types').IOCozyContact[]>} - Related contacts
*/
export const updateRelatedContact = async ({
client,
relation,
originalContactId
}) => {
try {
const contactsQueryById = buildContactsQueryById(relation._id)
const { data: relatedContact } = await client.query(
contactsQueryById.definition()
)
const relationConverted = makeRelationMapping(relation, originalContactId)
const updatedRelatedContact = setHasManyItem(
relatedContact,
'related',
originalContactId,
relationConverted
)

return updatedRelatedContact
} catch (error) {
log.error('Error updating related contact', error)
}
}

/**
* @param {Object} params
* @param {import('cozy-client/types/CozyClient').default} params.client - CozyClient
* @param {import('../types').RelatedRelationships} params.relation - Related relationships to update
* @param {string} params.originalContactId - Original contact id
* @returns {Promise<import('cozy-client/types/types').IOCozyContact[]>} - Related contacts
*/
export const deleteRelatedContact = async ({
client,
relation,
originalContactId
}) => {
try {
const contactsQueryById = buildContactsQueryById(relation._id)
const { data: relatedContact } = await client.query(
contactsQueryById.definition()
)
const updatedRelatedContact = removeHasManyItem(
relatedContact,
'related',
originalContactId
)

return updatedRelatedContact
} catch (error) {
log.error('Error deleting related contact', error)
}
}

/**
* @param {Object} params
* @param {import('cozy-client/types/CozyClient').default} params.client - CozyClient
* @param {import('../types').RelatedRelationshipsToUpdate[]} params.relatedRelationships - Related relationships to update
* @param {import('cozy-client/types/types').IOCozyContact} params.originalContact - Original contact
*/
export const updateRelatedRelationships = async ({
client,
relatedRelationships,
originalContactId
}) => {
return Promise.all(
relatedRelationships.map(relatedRel => {
switch (relatedRel.type) {
case 'CREATE':
return createRelatedContact({
client,
relation: relatedRel.relation,
originalContactId
})
case 'UPDATE':
return updateRelatedContact({
client,
relation: relatedRel.relation,
originalContactId
})
case 'DELETE':
return deleteRelatedContact({
client,
relation: relatedRel.relation,
originalContactId
})
}
})
)
}
Loading

0 comments on commit 0e679b7

Please sign in to comment.