Skip to content

Commit

Permalink
Support unmerge actions in audit logs (#2607)
Browse files Browse the repository at this point in the history
  • Loading branch information
skwowet authored Sep 19, 2024
1 parent ff0ff79 commit c37dc47
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 259 deletions.
371 changes: 199 additions & 172 deletions backend/src/services/memberService.ts

Large diffs are not rendered by default.

199 changes: 112 additions & 87 deletions backend/src/services/organizationService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { captureApiChange, organizationMergeAction } from '@crowd/audit-logs'
import {
captureApiChange,
organizationMergeAction,
organizationUnmergeAction,
} from '@crowd/audit-logs'
import { Error400, websiteNormalizer } from '@crowd/common'
import { hasLfxMembership } from '@crowd/data-access-layer/src/lfx_memberships'
import { LoggerBase } from '@crowd/logging'
Expand Down Expand Up @@ -216,106 +220,130 @@ export default class OrganizationService extends LoggerBase {
let tx

try {
const organization = await OrganizationRepository.findById(organizationId, this.options)
const { organization, secondaryOrganization } = await captureApiChange(
this.options,
organizationUnmergeAction(organizationId, async (captureOldState, captureNewState) => {
const organization = await OrganizationRepository.findById(organizationId, this.options)

const repoOptions: IRepositoryOptions =
await SequelizeRepository.createTransactionalRepositoryOptions(this.options)
tx = repoOptions.transaction
captureOldState({
primary: organization,
})

// remove identities in secondary organization from primary
await OrganizationRepository.removeIdentitiesFromOrganization(
organizationId,
payload.secondary.identities.filter(
(i) =>
i.verified === undefined || // backwards compatibility for old identity backups
i.verified === true ||
(i.verified === false &&
!payload.primary.identities.some(
(pi) =>
pi.verified === false &&
pi.platform === i.platform &&
pi.value === i.value &&
pi.type === i.type,
)),
),
repoOptions,
)
const repoOptions: IRepositoryOptions =
await SequelizeRepository.createTransactionalRepositoryOptions(this.options)
tx = repoOptions.transaction

// create the secondary org
const secondaryOrganization = await OrganizationRepository.create(
payload.secondary,
repoOptions,
)
// remove identities in secondary organization from primary
await OrganizationRepository.removeIdentitiesFromOrganization(
organizationId,
payload.secondary.identities.filter(
(i) =>
i.verified === undefined || // backwards compatibility for old identity backups
i.verified === true ||
(i.verified === false &&
!payload.primary.identities.some(
(pi) =>
pi.verified === false &&
pi.platform === i.platform &&
pi.value === i.value &&
pi.type === i.type,
)),
),
repoOptions,
)

await MergeActionsRepository.add(
MergeActionType.ORG,
organizationId,
secondaryOrganization.id,
this.options,
MergeActionStep.UNMERGE_STARTED,
MergeActionState.IN_PROGRESS,
)
// create the secondary org
const secondaryOrganization = await OrganizationRepository.create(
payload.secondary,
repoOptions,
)

if (payload.mergeActionId) {
const mergeAction = await MergeActionsRepository.findById(
payload.mergeActionId,
this.options,
)
await MergeActionsRepository.add(
MergeActionType.ORG,
organizationId,
secondaryOrganization.id,
this.options,
MergeActionStep.UNMERGE_STARTED,
MergeActionState.IN_PROGRESS,
)

if (mergeAction.unmergeBackup.secondary.memberOrganizations.length > 0) {
for (const role of mergeAction.unmergeBackup.secondary.memberOrganizations) {
await MemberOrganizationRepository.addMemberRole(
{ ...role, organizationId: secondaryOrganization.id },
repoOptions,
if (payload.mergeActionId) {
const mergeAction = await MergeActionsRepository.findById(
payload.mergeActionId,
this.options,
)

if (mergeAction.unmergeBackup.secondary.memberOrganizations.length > 0) {
for (const role of mergeAction.unmergeBackup.secondary.memberOrganizations) {
await MemberOrganizationRepository.addMemberRole(
{ ...role, organizationId: secondaryOrganization.id },
repoOptions,
)
}

const memberOrganizations =
await MemberOrganizationRepository.findRolesInOrganization(
organization.id,
repoOptions,
)

const primaryUnmergedRoles = await MemberOrganizationService.unmergeRoles(
memberOrganizations,
mergeAction.unmergeBackup.primary.memberOrganizations,
mergeAction.unmergeBackup.secondary.memberOrganizations,
MemberRoleUnmergeStrategy.SAME_ORGANIZATION,
)

// check if anything to delete in primary
const rolesToDelete = memberOrganizations.filter(
(r) =>
r.source !== 'ui' &&
!primaryUnmergedRoles.some(
(pr) =>
pr.memberId === r.memberId &&
pr.title === r.title &&
pr.dateStart === r.dateStart &&
pr.dateEnd === r.dateEnd,
),
)

for (const role of rolesToDelete) {
await MemberOrganizationRepository.removeMemberRole(role, repoOptions)
}
}
}

const memberOrganizations = await MemberOrganizationRepository.findRolesInOrganization(
organization.id,
repoOptions,
)
// delete identity related stuff, we already moved these
delete payload.primary.identities

const primaryUnmergedRoles = await MemberOrganizationService.unmergeRoles(
memberOrganizations,
mergeAction.unmergeBackup.primary.memberOrganizations,
mergeAction.unmergeBackup.secondary.memberOrganizations,
MemberRoleUnmergeStrategy.SAME_ORGANIZATION,
)
captureNewState({
primary: payload.primary,
secondary: secondaryOrganization,
})

// check if anything to delete in primary
const rolesToDelete = memberOrganizations.filter(
(r) =>
r.source !== 'ui' &&
!primaryUnmergedRoles.some(
(pr) =>
pr.memberId === r.memberId &&
pr.title === r.title &&
pr.dateStart === r.dateStart &&
pr.dateEnd === r.dateEnd,
),
// update rest of the primary org fields
await OrganizationRepository.update(
organizationId,
payload.primary,
repoOptions,
false,
false,
)

for (const role of rolesToDelete) {
await MemberOrganizationRepository.removeMemberRole(role, repoOptions)
}
}
}
// add primary and secondary to no merge so they don't get suggested again
await OrganizationRepository.addNoMerge(
organizationId,
secondaryOrganization.id,
repoOptions,
)

// delete identity related stuff, we already moved these
delete payload.primary.identities
// trigger entity-merging-worker to move activities in the background
await SequelizeRepository.commitTransaction(tx)

// update rest of the primary org fields
await OrganizationRepository.update(
organizationId,
payload.primary,
repoOptions,
false,
false,
return { organization, secondaryOrganization }
}),
)

// add primary and secondary to no merge so they don't get suggested again
await OrganizationRepository.addNoMerge(organizationId, secondaryOrganization.id, repoOptions)

await MergeActionsRepository.setMergeAction(
MergeActionType.ORG,
organizationId,
Expand All @@ -326,9 +354,6 @@ export default class OrganizationService extends LoggerBase {
},
)

// trigger entity-merging-worker to move activities in the background
await SequelizeRepository.commitTransaction(tx)

// responsible for moving organization's activities, syncing to opensearch afterwards, recalculating activity.organizationIds and notifying frontend via websockets
await this.options.temporal.workflow.start('finishOrganizationUnmerging', {
taskQueue: 'entity-merging',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ const options: SelectFilterOptionGroup[] = [
label: 'Profiles merged',
value: 'contributors-merged',
},
{
label: 'Profiles unmerged',
value: 'contributors-unmerged',
},
{
label: 'Profile identities updated',
value: 'contributor-identities-updated',
Expand Down Expand Up @@ -37,6 +41,10 @@ const options: SelectFilterOptionGroup[] = [
label: 'Organizations merged',
value: 'organizations-merged',
},
{
label: 'Organizations unmerged',
value: 'organizations-unmerged',
},
{
label: 'Organization identities updated',
value: 'organization-identities-updated',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import membersEditManualAffiliation from './members-edit-manual-affiliation';
import membersEditOrganizations from './members-edit-organizations';
import membersEditProfile from './members-edit-profile';
import membersMerge from './members-merge';
import membersUnmerge from './members-unmerge';
import organizationsCreate from './organizations-create';
import organizationsEditIdentities from './organizations-edit-identities';
import organizationsEditProfile from './organizations-edit-profile';
import organizationsMerge from './organizations-merge';
import organizationsUnmerge from './organizations-unmerge';

export interface LogRenderingConfig {
label: string;
Expand Down Expand Up @@ -39,10 +41,12 @@ export const logRenderingConfig: Record<ActionType, LogRenderingConfig> = {
[ActionType.MEMBERS_EDIT_ORGANIZATIONS]: membersEditOrganizations,
[ActionType.MEMBERS_EDIT_PROFILE]: membersEditProfile,
[ActionType.MEMBERS_MERGE]: membersMerge,
[ActionType.MEMBERS_UNMERGE]: membersUnmerge,

// Organizations
[ActionType.ORGANIZATIONS_CREATE]: organizationsCreate,
[ActionType.ORGANIZATIONS_EDIT_IDENTITIES]: organizationsEditIdentities,
[ActionType.ORGANIZATIONS_EDIT_PROFILE]: organizationsEditProfile,
[ActionType.ORGANIZATIONS_MERGE]: organizationsMerge,
[ActionType.ORGANIZATIONS_UNMERGE]: organizationsUnmerge,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index';

const membersMerge: LogRenderingConfig = {
label: 'Profiles unmerged',
changes: (log) => {
const primary = log.oldState?.primary;
const secondary = log.newState?.secondary;
const merged = log.newState?.primary;
return {
removals: merged ? [
`${primary?.displayName}<span> ・ ID: ${primary?.id}</span>`,
`${secondary?.displayName}<span> ・ ID: ${secondary?.id}</span>`,
] : [],
additions: merged ? [
`${merged?.displayName || primary?.displayName}<span> ・ ID: ${merged?.id || primary?.id}</span>`,
] : [],
changes: [],
};
},
description: (log) => {
const member = log.newState?.primary?.displayName || log.oldState?.primary?.displayName;

if (member) {
return `${member}<br>ID: ${log.entityId}`;
}

return '';
},
properties: (log) => {
const member = log.newState?.primary?.displayName || log.oldState?.primary?.displayName;

if (member) {
return [{
label: 'Profile',
value: `${member}<br><span>ID: ${log.entityId}</span>`,
}];
}
return [];
},
};

export default membersMerge;
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index';

const organizationsMerge: LogRenderingConfig = {
label: 'Organizations unmerged',
changes: (log) => {
const primary = log.oldState?.primary;
const secondary = log.newState?.secondary;
const merged = log.newState?.primary;
return {
removals: merged ? [
`${primary?.displayName}<span> ・ ID: ${primary?.id}</span>`,
`${secondary?.displayName}<span> ・ ID: ${secondary?.id}</span>`,
] : [],
additions: merged ? [
`${merged?.displayName || primary.displayName}<span> ・ ID: ${merged?.id || primary.id}</span>`,
] : [],
changes: [],
};
},
description: (log) => {
const organization = log.newState?.primary?.displayName || log.oldState?.primary?.displayName;
if (organization) {
return `${organization}<br>ID: ${log.entityId}`;
}
return '';
},
properties: (log) => {
const organization = log.newState?.primary?.displayName || log.oldState?.primary?.displayName;
if (organization) {
return [{
label: 'Organization',
value: `${organization}<br><span>ID: ${log.entityId}</span>`,
}];
}
return [];
},
};

export default organizationsMerge;
2 changes: 2 additions & 0 deletions frontend/src/modules/lf/segments/types/AuditLog.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export enum ActionType {
MEMBERS_MERGE = 'members-merge',
MEMBERS_UNMERGE = 'members-unmerge',
MEMBERS_EDIT_IDENTITIES = 'members-edit-identities',
MEMBERS_EDIT_ORGANIZATIONS = 'members-edit-organizations',
MEMBERS_EDIT_MANUAL_AFFILIATION = 'members-edit-manual-affiliation',
MEMBERS_EDIT_PROFILE = 'members-edit-profile',
MEMBERS_CREATE = 'members-create',
ORGANIZATIONS_MERGE = 'organizations-merge',
ORGANIZATIONS_UNMERGE = 'organizations-unmerge',
ORGANIZATIONS_EDIT_IDENTITIES = 'organizations-edit-identities',
ORGANIZATIONS_EDIT_PROFILE = 'organizations-edit-profile',
ORGANIZATIONS_CREATE = 'organizations-create',
Expand Down
Loading

0 comments on commit c37dc47

Please sign in to comment.