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

View File

@ -9,8 +9,6 @@ import { isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
@Injectable()
export class RestApiCreateManyHandler extends RestApiBaseHandler {
async handle(request: Request) {
@ -60,14 +58,6 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
const createdRecords = await repository.save(recordsToCreate);
this.apiEventEmitterService.emitCreateEvents({
records: createdRecords,
authContext: this.getAuthContextFromRequest(request),
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadata.objectMetadataMapItem,
),
});
const records = await this.getRecord({
recordIds: createdRecords.map((record) => record.id),
repository,

View File

@ -9,8 +9,6 @@ import { isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
@Injectable()
export class RestApiCreateOneHandler extends RestApiBaseHandler {
async handle(request: Request) {
@ -43,14 +41,6 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
const createdRecord = await repository.save(recordToCreate);
this.apiEventEmitterService.emitCreateEvents({
records: [createdRecord],
authContext: this.getAuthContextFromRequest(request),
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadata.objectMetadataMapItem,
),
});
const records = await this.getRecord({
recordIds: [createdRecord.id],
repository,

View File

@ -5,7 +5,6 @@ import { Request } from 'express';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
@Injectable()
export class RestApiDeleteOneHandler extends RestApiBaseHandler {
@ -24,14 +23,6 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler {
await repository.delete(recordId);
this.apiEventEmitterService.emitDestroyEvents({
records: [recordToDelete],
authContext: this.getAuthContextFromRequest(request),
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadata.objectMetadataMapItem,
),
});
return this.formatResult({
operation: 'delete',
objectNameSingular: objectMetadata.objectMetadataMapItem.nameSingular,

View File

@ -11,7 +11,6 @@ import {
} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
@ -28,15 +27,9 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
let objectRecords: Partial<ObjectRecord>[] = [];
if (request.body.ids) {
const nonFormattedObjectRecords = (await existingRecordsQueryBuilder
objectRecords = (await existingRecordsQueryBuilder
.where({ id: In(request.body.ids) })
.getMany()) as ObjectRecord[];
objectRecords = formatResult(
nonFormattedObjectRecords,
objectMetadataItemWithFieldsMaps,
objectMetadata.objectMetadataMaps,
);
} else if (request.body.data && !isEmpty(request.body.data)) {
objectRecords = request.body.data;
}

View File

@ -10,7 +10,6 @@ import { isDefined } from 'twenty-shared/utils';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
@Injectable()
export class RestApiUpdateOneHandler extends RestApiBaseHandler {
@ -38,16 +37,6 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler {
...overriddenBody,
});
this.apiEventEmitterService.emitUpdateEvents({
existingRecords: [recordToUpdate],
records: [updatedRecord],
updatedFields: Object.keys(request.body),
authContext: this.getAuthContextFromRequest(request),
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadata.objectMetadataMapItem,
),
});
const records = await this.getRecord({
recordIds: [updatedRecord.id],
repository,

View File

@ -12,7 +12,6 @@ import {
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
@ -80,8 +79,6 @@ export abstract class RestApiBaseHandler {
@Inject()
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
@Inject()
protected readonly apiEventEmitterService: ApiEventEmitterService;
@Inject()
protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService;
protected abstract handle(

View File

@ -1,25 +1,24 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler';
import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler';
import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler';
import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler';
import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler';
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler';
import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler';
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
const restApiCoreResolvers = [
RestApiDeleteOneHandler,
@ -46,7 +45,6 @@ const restApiCoreResolvers = [
providers: [
RestApiService,
RestApiCoreService,
ApiEventEmitterService,
...coreQueryBuilderFactories,
...restApiCoreResolvers,
],