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

@ -17,7 +17,6 @@ import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/g
import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service';
import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service';
import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@ -49,7 +48,6 @@ const graphqlQueryResolvers = [
UserRoleModule,
],
providers: [
ApiEventEmitterService,
ProcessNestedRelationsHelper,
ProcessNestedRelationsV2Helper,
ProcessAggregateHelper,

View File

@ -20,7 +20,6 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
@Injectable()
@ -180,8 +179,6 @@ export class ProcessNestedRelationsV2Helper {
: 'id',
ids: relationIds,
limit: limit * parentObjectRecords.length,
objectMetadataMaps,
targetObjectMetadata,
aggregate,
sourceFieldName,
});
@ -286,8 +283,6 @@ export class ProcessNestedRelationsV2Helper {
column,
ids,
limit,
objectMetadataMaps,
targetObjectMetadata,
aggregate,
sourceFieldName,
}: {
@ -297,8 +292,6 @@ export class ProcessNestedRelationsV2Helper {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ids: any[];
limit: number;
objectMetadataMaps: ObjectMetadataMaps;
targetObjectMetadata: ObjectMetadataItemWithFieldMaps;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
aggregate: Record<string, any>;
sourceFieldName: string;
@ -359,13 +352,7 @@ export class ProcessNestedRelationsV2Helper {
.take(limit)
.getMany();
const relationResults = formatResult<ObjectRecord[]>(
result,
targetObjectMetadata,
objectMetadataMaps,
);
return { relationResults, relationAggregatedFieldsResult };
return { relationResults: result, relationAggregatedFieldsResult };
}
private assignRelationResults({

View File

@ -19,7 +19,6 @@ import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/g
import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@ -66,8 +65,6 @@ export abstract class GraphqlQueryBaseResolverService<
@Inject()
protected readonly queryResultGettersFactory: QueryResultGettersFactory;
@Inject()
protected readonly apiEventEmitterService: ApiEventEmitterService;
@Inject()
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager;
@Inject()
protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper;
@ -128,6 +125,7 @@ export abstract class GraphqlQueryBaseResolverService<
objectMetadataItemWithFieldMaps.nameSingular,
shouldBypassPermissionChecks,
roleId,
authContext,
);
const graphqlQueryParser = new GraphqlQueryParser(

View File

@ -19,15 +19,11 @@ import {
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResolverService<
@ -48,7 +44,6 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
executionArgs,
objectRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const shouldBypassPermissionChecks = executionArgs.isExecutedByApiKey;
@ -105,21 +100,15 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
await this.processRecordsToUpdate({
partialRecordsToUpdate: recordsToUpdate,
existingRecords,
repository: executionArgs.repository,
objectMetadataItemWithFieldMaps,
objectMetadataMaps: executionArgs.options.objectMetadataMaps,
result,
authContext: executionArgs.options.authContext,
});
await this.processRecordsToInsert({
recordsToInsert,
repository: executionArgs.repository,
result,
objectMetadataItemWithFieldMaps,
objectMetadataMaps: executionArgs.options.objectMetadataMaps,
authContext: executionArgs.options.authContext,
});
return result;
@ -273,20 +262,14 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
private async processRecordsToUpdate({
partialRecordsToUpdate,
existingRecords,
repository,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
result,
authContext,
}: {
partialRecordsToUpdate: Partial<ObjectRecord>[];
existingRecords: Partial<ObjectRecord>[];
repository: WorkspaceRepository<ObjectLiteral>;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
result: InsertResult;
authContext: AuthContext;
}): Promise<void> {
for (const partialRecordToUpdate of partialRecordsToUpdate) {
const recordId = partialRecordToUpdate.id as string;
@ -298,101 +281,38 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
objectMetadataItemWithFieldMaps,
);
const formattedPartialRecordToUpdate = formatData(
await repository.update(
recordId,
partialRecordToUpdateWithoutCreatedByUpdate,
objectMetadataItemWithFieldMaps,
);
// TODO: we should align update and insert
// For insert, formating is done in the server
// While for update, formatting is done at the resolver level
await repository.update(recordId, formattedPartialRecordToUpdate);
result.identifiers.push({ id: recordId });
result.generatedMaps.push({ id: recordId });
const [updatedRecord] = await repository.find({
where: { id: recordId },
});
if (!isDefined(updatedRecord)) {
continue;
}
const record = formatResult<ObjectRecord>(
updatedRecord,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const existingRecord = formatResult<ObjectRecord>(
existingRecords.find((record) => record.id === recordId),
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
this.apiEventEmitterService.emitUpdateEvents({
existingRecords: structuredClone([existingRecord]),
records: structuredClone([record]),
updatedFields: Object.keys(formattedPartialRecordToUpdate),
authContext,
objectMetadataItem:
getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
});
}
}
private async processRecordsToInsert({
recordsToInsert,
repository,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
result,
authContext,
}: {
recordsToInsert: Partial<ObjectRecord>[];
repository: WorkspaceRepository<ObjectLiteral>;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
result: InsertResult;
authContext: AuthContext;
}): Promise<void> {
const formattedInsertedRecords: ObjectRecord[] = [];
if (recordsToInsert.length > 0) {
const insertResult = await repository.insert(recordsToInsert);
result.identifiers.push(...insertResult.identifiers);
result.generatedMaps.push(...insertResult.generatedMaps);
result.raw.push(...insertResult.raw);
formattedInsertedRecords.push(
...insertResult.raw.map((record: ObjectRecord) =>
formatResult<ObjectRecord>(
record,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
),
),
);
}
this.apiEventEmitterService.emitCreateEvents({
records: structuredClone(formattedInsertedRecords),
authContext,
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
});
}
private async fetchUpsertedRecords(
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
objectRecords: InsertResult,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<ObjectRecord[]> {
const queryBuilder = executionArgs.repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
@ -404,7 +324,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
objectMetadataItemWithFieldMaps,
});
const nonFormattedUpsertedRecords = await queryBuilder
const upsertedRecords = await queryBuilder
.setFindOptions({
select: columnsToSelect,
})
@ -414,11 +334,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
.take(QUERY_MAX_RECORDS)
.getMany();
return formatResult<ObjectRecord[]>(
nonFormattedUpsertedRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
return upsertedRecords as ObjectRecord[];
}
private async processNestedRelationsIfNeeded(

View File

@ -15,8 +15,6 @@ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/g
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolverService<
@ -48,7 +46,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
objectMetadataItemWithFieldMaps,
});
const nonFormattedUpsertedRecords = await queryBuilder
const upsertedRecords = (await queryBuilder
.setFindOptions({
select: columnsToSelect,
})
@ -56,21 +54,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
id: In(objectRecords.generatedMaps.map((record) => record.id)),
})
.take(QUERY_MAX_RECORDS)
.getMany();
const upsertedRecords = formatResult<ObjectRecord[]>(
nonFormattedUpsertedRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
this.apiEventEmitterService.emitCreateEvents({
records: structuredClone(upsertedRecords),
authContext,
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
});
.getMany()) as ObjectRecord[];
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({

View File

@ -14,8 +14,6 @@ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/g
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
@ -52,30 +50,17 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
objectMetadataItemWithFieldMaps,
});
const nonFormattedDeletedObjectRecords = await queryBuilder
const deletedObjectRecords = await queryBuilder
.softDelete()
.returning(columnsToReturn)
.execute();
const formattedDeletedRecords = formatResult<ObjectRecord[]>(
nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
this.apiEventEmitterService.emitDeletedEvents({
records: structuredClone(formattedDeletedRecords),
authContext,
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
});
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: formattedDeletedRecords,
parentObjectRecords:
deletedObjectRecords.generatedMaps as ObjectRecord[],
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
limit: QUERY_MAX_RECORDS,
authContext,
@ -89,7 +74,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return formattedDeletedRecords.map((record: ObjectRecord) =>
return deletedObjectRecords.generatedMaps.map((record: ObjectRecord) =>
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName: objectMetadataItemWithFieldMaps.nameSingular,

View File

@ -18,8 +18,6 @@ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/g
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolverService<
@ -44,34 +42,20 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
objectMetadataItemWithFieldMaps,
});
const nonFormattedDeletedObjectRecords = await queryBuilder
const deletedObjectRecords = await queryBuilder
.softDelete()
.where({ id: executionArgs.args.id })
.returning(columnsToReturn)
.execute();
const formattedDeletedRecords = formatResult<ObjectRecord[]>(
nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
if (formattedDeletedRecords.length === 0) {
if (deletedObjectRecords.generatedMaps.length === 0) {
throw new GraphqlQueryRunnerException(
'Record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const deletedRecord = formattedDeletedRecords[0];
this.apiEventEmitterService.emitDeletedEvents({
records: structuredClone(formattedDeletedRecords),
authContext,
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
});
const deletedRecord = deletedObjectRecords.generatedMaps[0] as ObjectRecord;
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({

View File

@ -12,8 +12,6 @@ import { DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolv
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
@ -50,30 +48,17 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
objectMetadataItemWithFieldMaps,
});
const nonFormattedDeletedObjectRecords = await queryBuilder
const deletedObjectRecords = await queryBuilder
.delete()
.returning(columnsToReturn)
.execute();
const deletedRecords = formatResult<ObjectRecord[]>(
nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
this.apiEventEmitterService.emitDestroyEvents({
records: structuredClone(deletedRecords),
authContext,
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
});
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: deletedRecords,
parentObjectRecords:
deletedObjectRecords.generatedMaps as ObjectRecord[],
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
limit: QUERY_MAX_RECORDS,
authContext,
@ -87,7 +72,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return deletedRecords.map((record: ObjectRecord) =>
return deletedObjectRecords.generatedMaps.map((record: ObjectRecord) =>
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName: objectMetadataItemWithFieldMaps.nameSingular,

View File

@ -16,8 +16,6 @@ import {
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResolverService<
@ -42,38 +40,25 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
objectMetadataItemWithFieldMaps,
});
const nonFormattedDeletedObjectRecords = await queryBuilder
const deletedObjectRecords = await queryBuilder
.delete()
.where({ id: executionArgs.args.id })
.returning(columnsToReturn)
.execute();
if (!nonFormattedDeletedObjectRecords.affected) {
if (!deletedObjectRecords.affected) {
throw new GraphqlQueryRunnerException(
'Record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const deletedRecords = formatResult<ObjectRecord[]>(
nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
this.apiEventEmitterService.emitDestroyEvents({
records: structuredClone(deletedRecords),
authContext,
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
});
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: deletedRecords,
parentObjectRecords:
deletedObjectRecords.generatedMaps as ObjectRecord[],
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
limit: QUERY_MAX_RECORDS,
authContext,
@ -88,7 +73,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return typeORMObjectRecordsParser.processRecord({
objectRecord: deletedRecords[0],
objectRecord: deletedObjectRecords.generatedMaps[0] as ObjectRecord,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,

View File

@ -24,8 +24,6 @@ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/g
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService<
@ -67,20 +65,11 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
let objectRecords: Partial<ObjectRecord>[] = [];
if (executionArgs.args.ids) {
const nonFormattedObjectRecords = (await existingRecordsQueryBuilder
objectRecords = (await existingRecordsQueryBuilder
.where({ id: In(executionArgs.args.ids) })
.getMany()) as ObjectRecord[];
objectRecords = formatResult(
nonFormattedObjectRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
} else if (executionArgs.args.data && !isEmpty(executionArgs.args.data)) {
objectRecords = formatData(
executionArgs.args.data,
objectMetadataItemWithFieldMaps,
);
objectRecords = executionArgs.args.data;
}
const duplicateConnections: IConnection<ObjectRecord>[] = await Promise.all(
@ -120,18 +109,12 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
duplicateConditions,
);
const nonFormattedDuplicates = (await duplicateRecordsQueryBuilder
const duplicates = (await duplicateRecordsQueryBuilder
.setFindOptions({
select: columnsToSelect,
})
.getMany()) as ObjectRecord[];
const duplicates = formatResult<ObjectRecord[]>(
nonFormattedDuplicates,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
return typeORMObjectRecordsParser.createConnection({
objectRecords: duplicates,
objectName: objectMetadataItemWithFieldMaps.nameSingular,

View File

@ -29,7 +29,6 @@ import {
getPaginationInfo,
} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolverService<
@ -127,18 +126,12 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
objectMetadataItemWithFieldMaps,
});
const nonFormattedObjectRecords = await queryBuilder
const objectRecords = (await queryBuilder
.setFindOptions({
select: columnsToSelect,
})
.take(limit + 1)
.getMany();
const objectRecords = formatResult<ObjectRecord[]>(
nonFormattedObjectRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
.getMany()) as ObjectRecord[];
const { hasNextPage, hasPreviousPage } = getPaginationInfo(
objectRecords,

View File

@ -23,7 +23,6 @@ import {
WorkspaceQueryRunnerException,
WorkspaceQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolverService<
@ -59,18 +58,12 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver
objectMetadataItemWithFieldMaps,
});
const nonFormattedObjectRecord = await queryBuilder
const objectRecord = await queryBuilder
.setFindOptions({
select: columnsToSelect,
})
.getOne();
const objectRecord = formatResult<ObjectRecord>(
nonFormattedObjectRecord,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
if (!objectRecord) {
throw new GraphqlQueryRunnerException(
'Record not found',
@ -78,7 +71,7 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver
);
}
const objectRecords = [objectRecord];
const objectRecords = [objectRecord] as ObjectRecord[];
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({

View File

@ -14,8 +14,6 @@ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/g
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
@ -52,30 +50,17 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
objectMetadataItemWithFieldMaps,
});
const nonFormattedRestoredObjectRecords = await queryBuilder
const restoredObjectRecords = await queryBuilder
.restore()
.returning(columnsToReturn)
.execute();
const formattedRestoredRecords = formatResult<ObjectRecord[]>(
nonFormattedRestoredObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
this.apiEventEmitterService.emitRestoreEvents({
records: structuredClone(formattedRestoredRecords),
authContext,
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
});
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: formattedRestoredRecords,
parentObjectRecords:
restoredObjectRecords.generatedMaps as ObjectRecord[],
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
limit: QUERY_MAX_RECORDS,
authContext,
@ -89,7 +74,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return formattedRestoredRecords.map((record: ObjectRecord) =>
return restoredObjectRecords.generatedMaps.map((record: ObjectRecord) =>
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName: objectMetadataItemWithFieldMaps.nameSingular,

View File

@ -18,8 +18,6 @@ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/g
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResolverService<
@ -44,34 +42,21 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
objectMetadataItemWithFieldMaps,
});
const nonFormattedRestoredObjectRecords = await queryBuilder
const restoredObjectRecords = await queryBuilder
.restore()
.where({ id: executionArgs.args.id })
.returning(columnsToReturn)
.execute();
const formattedRestoredRecords = formatResult<ObjectRecord[]>(
nonFormattedRestoredObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
if (formattedRestoredRecords.length === 0) {
if (restoredObjectRecords.generatedMaps.length === 0) {
throw new GraphqlQueryRunnerException(
'Record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const restoredRecord = formattedRestoredRecords[0];
this.apiEventEmitterService.emitRestoreEvents({
records: structuredClone(formattedRestoredRecords),
authContext,
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
});
const restoredRecord = restoredObjectRecords
.generatedMaps[0] as ObjectRecord;
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import isEmpty from 'lodash.isempty';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import {
@ -11,17 +10,10 @@ import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/int
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
@ -41,29 +33,6 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
objectMetadataItemWithFieldMaps.nameSingular,
);
const existingRecordsBuilder = queryBuilder.clone();
executionArgs.graphqlQueryParser.applyFilterToBuilder(
existingRecordsBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
executionArgs.args.filter,
);
const existingRecords = await existingRecordsBuilder.getMany();
const formattedExistingRecords = formatResult<ObjectRecord[]>(
existingRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
if (isEmpty(formattedExistingRecords)) {
throw new GraphqlQueryRunnerException(
'Records not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
@ -75,46 +44,24 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
executionArgs.args.filter,
);
const data = formatData(
executionArgs.args.data,
objectMetadataItemWithFieldMaps,
);
const columnsToReturn = buildColumnsToReturn({
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
});
const nonFormattedUpdatedObjectRecords = await queryBuilder
.update(data)
const updatedObjectRecords = await queryBuilder
.update()
.set(executionArgs.args.data)
.returning(columnsToReturn)
.execute();
const formattedUpdatedRecords = formatResult<ObjectRecord[]>(
nonFormattedUpdatedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
this.apiEventEmitterService.emitUpdateEvents({
existingRecords: structuredClone(formattedExistingRecords),
records: structuredClone(formattedUpdatedRecords),
updatedFields: Object.keys(executionArgs.args.data),
authContext,
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
});
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: [
...formattedExistingRecords,
...formattedUpdatedRecords,
],
parentObjectRecords:
updatedObjectRecords.generatedMaps as ObjectRecord[],
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
limit: QUERY_MAX_RECORDS,
authContext,
@ -128,7 +75,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return formattedUpdatedRecords.map((record: ObjectRecord) =>
return updatedObjectRecords.generatedMaps.map((record: ObjectRecord) =>
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName: objectMetadataItemWithFieldMaps.nameSingular,

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import isEmpty from 'lodash.isempty';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import {
@ -19,9 +18,6 @@ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/g
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolverService<
@ -40,73 +36,33 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
objectMetadataItemWithFieldMaps.nameSingular,
);
const data = formatData(
executionArgs.args.data,
objectMetadataItemWithFieldMaps,
);
const existingRecordBuilder = queryBuilder.clone();
const existingRecords = (await existingRecordBuilder
.where({ id: executionArgs.args.id })
.getMany()) as ObjectRecord[];
const formattedExistingRecords = formatResult<ObjectRecord[]>(
existingRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
if (isEmpty(formattedExistingRecords)) {
throw new GraphqlQueryRunnerException(
'Record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const columnsToReturn = buildColumnsToReturn({
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
});
const nonFormattedUpdatedObjectRecords = await queryBuilder
.update(data)
const updatedObjectRecords = await queryBuilder
.update()
.set(executionArgs.args.data)
.where({ id: executionArgs.args.id })
.returning(columnsToReturn)
.execute();
const formattedUpdatedRecords = formatResult<ObjectRecord[]>(
nonFormattedUpdatedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const updatedRecord = updatedObjectRecords.generatedMaps[0] as ObjectRecord;
if (formattedUpdatedRecords.length === 0) {
if (!updatedRecord) {
throw new GraphqlQueryRunnerException(
'Record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const updatedRecord = formattedUpdatedRecords[0];
const existingRecord = formattedExistingRecords[0];
this.apiEventEmitterService.emitUpdateEvents({
existingRecords: structuredClone(formattedExistingRecords),
records: structuredClone(formattedUpdatedRecords),
updatedFields: Object.keys(executionArgs.args.data),
authContext,
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
objectMetadataItemWithFieldMaps,
),
});
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: [existingRecord, updatedRecord],
parentObjectRecords: [updatedRecord],
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
limit: QUERY_MAX_RECORDS,
authContext,

View File

@ -1,188 +0,0 @@
import { Injectable } from '@nestjs/common';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
@Injectable()
export class ApiEventEmitterService {
constructor(private readonly workspaceEventEmitter: WorkspaceEventEmitter) {}
public emitCreateEvents<T extends ObjectRecord>({
records,
authContext,
objectMetadataItem,
}: {
records: T[];
authContext: AuthContext;
objectMetadataItem: ObjectMetadataInterface;
}): void {
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: objectMetadataItem.nameSingular,
action: DatabaseEventAction.CREATED,
events: records.map((record) => ({
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
before: null,
after: record,
},
})),
workspaceId: authContext.workspace?.id,
});
}
public emitUpdateEvents<T extends ObjectRecord>({
existingRecords,
records,
updatedFields,
authContext,
objectMetadataItem,
}: {
existingRecords: T[];
records: T[];
updatedFields: string[];
authContext: AuthContext;
objectMetadataItem: ObjectMetadataInterface;
}): void {
const workspace = authContext.workspace;
workspaceValidator.assertIsDefinedOrThrow(workspace);
const mappedExistingRecords = existingRecords.reduce(
(acc, { id, ...record }) => ({
...acc,
[id]: record,
}),
{},
);
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: objectMetadataItem.nameSingular,
action: DatabaseEventAction.UPDATED,
events: records.map((record) => {
// @ts-expect-error legacy noImplicitAny
const before = mappedExistingRecords[record.id];
const after = record;
const diff = objectRecordChangedValues(
before,
after,
updatedFields,
objectMetadataItem,
);
return {
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
before,
after,
updatedFields,
diff,
},
};
}),
workspaceId: workspace.id,
});
}
public emitDeletedEvents<T extends ObjectRecord>({
records,
authContext,
objectMetadataItem,
}: {
records: T[];
authContext: AuthContext;
objectMetadataItem: ObjectMetadataInterface;
}): void {
const workspace = authContext.workspace;
workspaceValidator.assertIsDefinedOrThrow(workspace);
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: objectMetadataItem.nameSingular,
action: DatabaseEventAction.DELETED,
events: records.map((record) => {
return {
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
before: record,
after: null,
},
};
}),
workspaceId: workspace.id,
});
}
public emitRestoreEvents<T extends ObjectRecord>({
records,
authContext,
objectMetadataItem,
}: {
records: T[];
authContext: AuthContext;
objectMetadataItem: ObjectMetadataInterface;
}): void {
const workspace = authContext.workspace;
workspaceValidator.assertIsDefinedOrThrow(workspace);
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: objectMetadataItem.nameSingular,
action: DatabaseEventAction.RESTORED,
events: records.map((record) => {
return {
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
before: null,
after: record,
},
};
}),
workspaceId: workspace.id,
});
}
public emitDestroyEvents<T extends ObjectRecord>({
records,
authContext,
objectMetadataItem,
}: {
records: T[];
authContext: AuthContext;
objectMetadataItem: ObjectMetadataInterface;
}): void {
const workspace = authContext.workspace;
workspaceValidator.assertIsDefinedOrThrow(workspace);
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: objectMetadataItem.nameSingular,
action: DatabaseEventAction.DESTROYED,
events: records.map((record) => {
return {
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
before: record,
after: null,
},
};
}),
workspaceId: workspace.id,
});
}
}