Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support unmerge actions in audit logs #2607

Merged
merged 10 commits into from
Sep 19, 2024
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
Loading