Add db event emitter in twenty orm (#13167)

## Context
Add an eventEmitter instance to twenty datasources so we can emit DB
events.
Add input and output formatting to twenty orm (formatData, formatResult)
Those 2 elements simplified existing logic when we interact with the
ORM, input will be formatted by the ORM so we can directly use
field-like structure instead of column-like. The output will be
formatted, for builder queries it will be in `result.generatedMaps`
where `result.raw` preserves the previous column-like structure.

Important change: We now have an authContext that we can pass when we
get a repository, this will be used for the different events emitted in
the ORM. We also removed the caching for repositories as it was not
scaling well and not necessary imho

Note: An upcoming PR should handle the onDelete: cascade behavior where
we send DESTROY events in cascade when there is an onDelete: CASCADE on
the FK.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2025-07-17 18:07:28 +02:00
committed by GitHub
parent 4a3139c9e0
commit 2deac9448e
79 changed files with 1061 additions and 2016 deletions
@@ -20,18 +20,15 @@ describe('MatchParticipantService', () => {
find: jest.Mock;
update: jest.Mock;
createQueryBuilder: jest.Mock;
formatResult: jest.Mock;
};
let mockCalendarEventParticipantRepository: {
find: jest.Mock;
update: jest.Mock;
createQueryBuilder: jest.Mock;
formatResult: jest.Mock;
};
let mockPersonRepository: {
find: jest.Mock;
createQueryBuilder: jest.Mock;
formatResult: jest.Mock;
};
let mockWorkspaceMemberRepository: {
find: jest.Mock;
@@ -53,7 +50,6 @@ describe('MatchParticipantService', () => {
getMany: jest.fn(),
withDeleted: jest.fn().mockReturnThis(),
}),
formatResult: jest.fn(),
};
mockCalendarEventParticipantRepository = {
@@ -68,7 +64,6 @@ describe('MatchParticipantService', () => {
getMany: jest.fn(),
withDeleted: jest.fn().mockReturnThis(),
}),
formatResult: jest.fn(),
};
mockPersonRepository = {
@@ -82,7 +77,6 @@ describe('MatchParticipantService', () => {
getMany: jest.fn(),
withDeleted: jest.fn().mockReturnThis(),
}),
formatResult: jest.fn(),
};
mockWorkspaceMemberRepository = {
@@ -204,7 +198,7 @@ describe('MatchParticipantService', () => {
};
mockPersonRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
mockPersonRepository.formatResult.mockResolvedValue(mockPeople);
mockQueryBuilder.getMany.mockResolvedValue(mockPeople);
mockWorkspaceMemberRepository.find.mockResolvedValue(
mockWorkspaceMembers,
);
@@ -316,7 +310,18 @@ describe('MatchParticipantService', () => {
});
it('should handle participants with no matching people or workspace members', async () => {
mockPersonRepository.formatResult.mockResolvedValue([]);
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue([]),
withDeleted: jest.fn().mockReturnThis(),
};
mockPersonRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
mockQueryBuilder.getMany.mockResolvedValue([]);
mockWorkspaceMemberRepository.find.mockResolvedValue([]);
await service.matchParticipants({
@@ -545,7 +550,18 @@ describe('MatchParticipantService', () => {
affected: 1,
});
mockMessageParticipantRepository.find.mockResolvedValue([]);
mockPersonRepository.formatResult.mockResolvedValue([]);
const mockQueryBuilder = {
select: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue([]),
withDeleted: jest.fn().mockReturnThis(),
};
mockPersonRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
mockQueryBuilder.getMany.mockResolvedValue([]);
});
describe('person unmatching', () => {
@@ -590,9 +606,7 @@ describe('MatchParticipantService', () => {
mockPersonRepository.createQueryBuilder.mockReturnValue(
mockQueryBuilder,
);
mockPersonRepository.formatResult.mockResolvedValue(
mockAlternativePeople,
);
mockQueryBuilder.getMany.mockResolvedValue(mockAlternativePeople);
const rematchedParticipants = [
{
@@ -655,7 +669,7 @@ describe('MatchParticipantService', () => {
mockPersonRepository.createQueryBuilder.mockReturnValue(
mockQueryBuilder,
);
mockPersonRepository.formatResult.mockResolvedValue([]);
mockQueryBuilder.getMany.mockResolvedValue([]);
await service.unmatchParticipants({
handle: 'test-1@example.com',
@@ -82,12 +82,10 @@ export class MatchParticipantService<
emails: uniqueParticipantsHandles,
});
const rawPeople = await queryBuilder
const people = await queryBuilder
.orderBy('person.createdAt', 'ASC')
.getMany();
const people = await personRepository.formatResult(rawPeople);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
@@ -192,12 +190,10 @@ export class MatchParticipantService<
excludePersonIds: [personId],
});
const rawPeople = await queryBuilder
const peopleToMatch = await queryBuilder
.orderBy('person.createdAt', 'ASC')
.getMany();
const peopleToMatch = await personRepository.formatResult(rawPeople);
if (peopleToMatch.length > 0) {
const bestMatch = findPersonByPrimaryOrAdditionalEmail({
people: peopleToMatch,