Skip to content

Commit 77dda2d

Browse files
committed
Emit restore events for revived upsert records
1 parent 76c1eed commit 77dda2d

2 files changed

Lines changed: 62 additions & 3 deletions

File tree

packages/twenty-server/src/engine/twenty-orm/repository/workspace-repository.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ const MUTATION_EVENT_ACTIONS_BY_KIND: Record<
8686
delete: [DatabaseEventAction.DESTROYED],
8787
restore: [DatabaseEventAction.RESTORED],
8888
'soft-delete': [DatabaseEventAction.DELETED],
89-
update: [DatabaseEventAction.UPDATED, DatabaseEventAction.UPSERTED],
89+
update: [
90+
DatabaseEventAction.UPDATED,
91+
DatabaseEventAction.RESTORED,
92+
DatabaseEventAction.UPSERTED,
93+
],
9094
};
9195

9296
type WorkspaceRepositoryOptions<TEntity extends ObjectLiteral> = {
@@ -1439,16 +1443,40 @@ export class WorkspaceRepository<TEntity extends ObjectLiteral = ObjectRecord> {
14391443
const formattedAfter = isDefined(recordsAfter)
14401444
? this.formatResult<ObjectRecord[]>(recordsAfter)
14411445
: undefined;
1446+
const recordBeforeById = new Map(
1447+
formattedBefore.map((record) => [record.id, record]),
1448+
);
1449+
1450+
const restoredRecordIds = new Set(
1451+
formattedAfter
1452+
?.filter((recordAfter) => {
1453+
const recordBefore = recordBeforeById.get(recordAfter.id);
1454+
1455+
return (
1456+
isDefined(recordBefore?.deletedAt) && recordAfter.deletedAt === null
1457+
);
1458+
})
1459+
.map(({ id }) => id),
1460+
);
14421461

14431462
for (const action of actions) {
1463+
const recordsAfterForAction =
1464+
kind !== 'update' || action === DatabaseEventAction.UPSERTED
1465+
? formattedAfter
1466+
: formattedAfter?.filter(({ id }) =>
1467+
action === DatabaseEventAction.RESTORED
1468+
? restoredRecordIds.has(id)
1469+
: !restoredRecordIds.has(id),
1470+
);
1471+
14441472
const event = formatTwentyOrmEventToDatabaseBatchEvent({
14451473
action,
14461474
objectMetadataItem: this.options.flatObjectMetadata,
14471475
flatFieldMetadataMaps:
14481476
this.options.internalContext.flatFieldMetadataMaps,
14491477
workspaceId: this.options.internalContext.workspaceId,
14501478
recordsBefore: formattedBefore,
1451-
recordsAfter: formattedAfter,
1479+
recordsAfter: recordsAfterForAction,
14521480
authContext: this.options.authContext,
14531481
});
14541482

packages/twenty-server/test/integration/graphql/suites/timeline/timeline-activity-write-path.integration-spec.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
2+
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
23
import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util';
34
import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util';
45
import { updateManyOperationFactory } from 'test/integration/graphql/utils/update-many-operation-factory.util';
@@ -563,7 +564,7 @@ describe('timeline activity write path (integration)', () => {
563564
});
564565
});
565566

566-
it('should write a linked entry on the company when the note target is deleted', async () => {
567+
it('should write the unlink and re-link entries when a note target is deleted and upserted', async () => {
567568
await makeGraphqlAPIRequest(
568569
deleteOneOperationFactory({
569570
objectMetadataSingularName: 'noteTarget',
@@ -584,6 +585,36 @@ describe('timeline activity write path (integration)', () => {
584585

585586
expect(timelineActivities).toHaveLength(1);
586587
expect(timelineActivities[0].linkedRecordId).toBe(NOTE_ID);
588+
589+
const restoreResponse = await makeGraphqlAPIRequest(
590+
createManyOperationFactory({
591+
objectMetadataSingularName: 'noteTarget',
592+
objectMetadataPluralName: 'noteTargets',
593+
gqlFields: 'id',
594+
data: [
595+
{
596+
noteId: NOTE_ID,
597+
targetCompanyId: NOTE_COMPANY_ID,
598+
},
599+
],
600+
upsert: true,
601+
}),
602+
);
603+
604+
expect(restoreResponse.body.errors).toBeUndefined();
605+
606+
const linkedTimelineActivities = await findTimelineActivities({
607+
targetCompanyId: { eq: NOTE_COMPANY_ID },
608+
timelineActivityTypeId: {
609+
eq: timelineActivityTypeIdForOrThrow(
610+
'linked',
611+
NOTE_UNIVERSAL_IDENTIFIER,
612+
),
613+
},
614+
});
615+
616+
expect(linkedTimelineActivities).toHaveLength(2);
617+
expect(linkedTimelineActivities[1].linkedRecordId).toBe(NOTE_ID);
587618
});
588619
});
589620

0 commit comments

Comments
 (0)