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
@@ -97,24 +97,20 @@ describe('WorkspaceRepository', () => {
id: 'test-metadata-id',
nameSingular: 'test-entity',
namePlural: 'test-entities',
fields: [],
fieldIdByName: {
id: 'test-field-id',
},
fieldIdByJoinColumnName: {},
fieldsById: {
'test-field-id': {
id: 'test-field-id',
name: 'id',
type: 'string',
isNullable: false,
isUnique: true,
},
},
});
jest.spyOn(repository as any, 'formatData').mockImplementation((data) => {
if (Array.isArray(data)) {
return data.map((item) => Object.assign({}, item));
}
return Object.assign({}, data);
});
jest.spyOn(repository as any, 'formatResult').mockImplementation((data) => {
if (Array.isArray(data)) {
return data.map((item) => Object.assign({}, item));
}
return Object.assign({}, data);
});
});
describe('Find Methods', () => {
@@ -239,7 +235,7 @@ describe('WorkspaceRepository', () => {
it('should delegate to workspaceEntityManager delete', async () => {
const criteria: FindOptionsWhere<ObjectLiteral> = { id: 'test-id' };
const expectedResult = { affected: 1, raw: [] };
const expectedResult = { affected: 1, raw: [], generatedMaps: [] };
mockEntityManager.delete.mockResolvedValue(expectedResult);
@@ -2,6 +2,7 @@ import { ObjectRecordsPermissions } from 'twenty-shared/types';
import {
DeleteQueryBuilder,
DeleteResult,
EntityTarget,
InsertQueryBuilder,
ObjectLiteral,
} from 'typeorm';
@@ -9,10 +10,18 @@ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.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 {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-soft-delete-query-builder';
import { WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
export class WorkspaceDeleteQueryBuilder<
T extends ObjectLiteral,
@@ -20,16 +29,19 @@ export class WorkspaceDeleteQueryBuilder<
private objectRecordsPermissions: ObjectRecordsPermissions;
private shouldBypassPermissionChecks: boolean;
private internalContext: WorkspaceInternalContext;
private authContext?: AuthContext;
constructor(
queryBuilder: DeleteQueryBuilder<T>,
objectRecordsPermissions: ObjectRecordsPermissions,
internalContext: WorkspaceInternalContext,
shouldBypassPermissionChecks: boolean,
authContext?: AuthContext,
) {
super(queryBuilder);
this.objectRecordsPermissions = objectRecordsPermissions;
this.internalContext = internalContext;
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
this.authContext = authContext;
}
override clone(): this {
@@ -40,10 +52,11 @@ export class WorkspaceDeleteQueryBuilder<
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
) as this;
}
override execute(): Promise<DeleteResult> {
override async execute(): Promise<DeleteResult> {
validateQueryIsPermittedOrThrow(
this.expressionMap,
this.objectRecordsPermissions,
@@ -51,11 +64,54 @@ export class WorkspaceDeleteQueryBuilder<
this.shouldBypassPermissionChecks,
);
return super.execute();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const result = await super.execute();
const formattedResult = formatResult<T[]>(
result.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.DESTROYED,
objectMetadataItem: objectMetadata,
workspaceId: this.internalContext.workspaceId,
entities: formattedResult,
authContext: this.authContext,
});
return {
raw: result.raw,
generatedMaps: formattedResult,
affected: result.affected,
};
}
private getMainAliasTarget(): EntityTarget<T> {
const mainAliasTarget = this.expressionMap.mainAlias?.target;
if (!mainAliasTarget) {
throw new TwentyORMException(
'Main alias target is missing',
TwentyORMExceptionCode.MISSING_MAIN_ALIAS_TARGET,
);
}
return mainAliasTarget;
}
override select(): WorkspaceSelectQueryBuilder<T> {
throw new Error('This builder cannot morph into a select builder');
throw new TwentyORMException(
'This builder cannot morph into a select builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override update(): WorkspaceUpdateQueryBuilder<T>;
@@ -67,18 +123,30 @@ export class WorkspaceDeleteQueryBuilder<
override update(
_updateSet?: QueryDeepPartialEntity<T>,
): WorkspaceUpdateQueryBuilder<T> {
throw new Error('This builder cannot morph into an update builder');
throw new TwentyORMException(
'This builder cannot morph into an update builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override insert(): InsertQueryBuilder<T> {
throw new Error('This builder cannot morph into an insert builder');
throw new TwentyORMException(
'This builder cannot morph into an insert builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override softDelete(): WorkspaceSoftDeleteQueryBuilder<T> {
throw new Error('This builder cannot morph into a soft delete builder');
throw new TwentyORMException(
'This builder cannot morph into a soft delete builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override restore(): WorkspaceSoftDeleteQueryBuilder<T> {
throw new Error('This builder cannot morph into a soft delete builder');
throw new TwentyORMException(
'This builder cannot morph into a soft delete builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
}
@@ -1,13 +1,28 @@
import { ObjectRecordsPermissions } from 'twenty-shared/types';
import { InsertQueryBuilder, ObjectLiteral } from 'typeorm';
import {
EntityTarget,
InsertQueryBuilder,
InsertResult,
ObjectLiteral,
} from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.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 {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
import { WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-delete-query-builder';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-soft-delete-query-builder';
import { WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
export class WorkspaceInsertQueryBuilder<
T extends ObjectLiteral,
@@ -15,17 +30,20 @@ export class WorkspaceInsertQueryBuilder<
private objectRecordsPermissions: ObjectRecordsPermissions;
private shouldBypassPermissionChecks: boolean;
private internalContext: WorkspaceInternalContext;
private authContext?: AuthContext;
constructor(
queryBuilder: InsertQueryBuilder<T>,
objectRecordsPermissions: ObjectRecordsPermissions,
internalContext: WorkspaceInternalContext,
shouldBypassPermissionChecks: boolean,
authContext?: AuthContext,
) {
super(queryBuilder);
this.objectRecordsPermissions = objectRecordsPermissions;
this.internalContext = internalContext;
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
this.authContext = authContext;
}
override clone(): this {
@@ -36,11 +54,26 @@ export class WorkspaceInsertQueryBuilder<
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
) as this;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override execute(): Promise<any> {
override values(
values: QueryDeepPartialEntity<T> | QueryDeepPartialEntity<T>[],
): this {
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const formattedValues = formatData(values, objectMetadata);
return super.values(formattedValues);
}
override async execute(): Promise<InsertResult> {
validateQueryIsPermittedOrThrow(
this.expressionMap,
this.objectRecordsPermissions,
@@ -48,26 +81,81 @@ export class WorkspaceInsertQueryBuilder<
this.shouldBypassPermissionChecks,
);
return super.execute();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const result = await super.execute();
const formattedResult = formatResult<T[]>(
result.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.CREATED,
objectMetadataItem: objectMetadata,
workspaceId: this.internalContext.workspaceId,
entities: formattedResult,
authContext: this.authContext,
});
return {
raw: result.raw,
generatedMaps: formattedResult,
identifiers: result.identifiers,
};
}
private getMainAliasTarget(): EntityTarget<T> {
const mainAliasTarget = this.expressionMap.mainAlias?.target;
if (!mainAliasTarget) {
throw new TwentyORMException(
'Main alias target is missing',
TwentyORMExceptionCode.MISSING_MAIN_ALIAS_TARGET,
);
}
return mainAliasTarget;
}
override select(): WorkspaceSelectQueryBuilder<T> {
throw new Error('This builder cannot morph into a select builder');
throw new TwentyORMException(
'This builder cannot morph into a select builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override update(): WorkspaceUpdateQueryBuilder<T> {
throw new Error('This builder cannot morph into an update builder');
throw new TwentyORMException(
'This builder cannot morph into an update builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override delete(): WorkspaceDeleteQueryBuilder<T> {
throw new Error('This builder cannot morph into a delete builder');
throw new TwentyORMException(
'This builder cannot morph into a delete builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override softDelete(): WorkspaceSoftDeleteQueryBuilder<T> {
throw new Error('This builder cannot morph into a soft delete builder');
throw new TwentyORMException(
'This builder cannot morph into a soft delete builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override restore(): WorkspaceSoftDeleteQueryBuilder<T> {
throw new Error('This builder cannot morph into a soft delete builder');
throw new TwentyORMException(
'This builder cannot morph into a soft delete builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
}
@@ -1,18 +1,25 @@
import { ObjectRecordsPermissions } from 'twenty-shared/types';
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
import { EntityTarget, ObjectLiteral, SelectQueryBuilder } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
import { WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-delete-query-builder';
import { WorkspaceInsertQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-insert-query-builder';
import { WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-soft-delete-query-builder';
import { WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
export class WorkspaceSelectQueryBuilder<
T extends ObjectLiteral,
@@ -20,16 +27,19 @@ export class WorkspaceSelectQueryBuilder<
objectRecordsPermissions: ObjectRecordsPermissions;
shouldBypassPermissionChecks: boolean;
internalContext: WorkspaceInternalContext;
authContext?: AuthContext;
constructor(
queryBuilder: SelectQueryBuilder<T>,
objectRecordsPermissions: ObjectRecordsPermissions,
internalContext: WorkspaceInternalContext,
shouldBypassPermissionChecks: boolean,
authContext?: AuthContext,
) {
super(queryBuilder);
this.objectRecordsPermissions = objectRecordsPermissions;
this.internalContext = internalContext;
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
this.authContext = authContext;
}
getFindOptions() {
@@ -44,19 +54,55 @@ export class WorkspaceSelectQueryBuilder<
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
) as this;
}
override execute(): Promise<T[]> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override async execute(): Promise<any> {
this.validatePermissions();
return super.execute();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const result = await super.execute();
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return {
raw: result,
generatedMaps: formattedResult,
identifiers: result.identifiers,
};
}
override getMany(): Promise<T[]> {
override async getMany(): Promise<T[]> {
this.validatePermissions();
return super.getMany();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const result = await super.getMany();
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return formattedResult;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -73,16 +119,46 @@ export class WorkspaceSelectQueryBuilder<
return super.getRawMany();
}
override getOne(): Promise<T | null> {
override async getOne(): Promise<T | null> {
this.validatePermissions();
return super.getOne();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const result = await super.getOne();
const formattedResult = formatResult<T>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return formattedResult;
}
override getOneOrFail(): Promise<T> {
override async getOneOrFail(): Promise<T> {
this.validatePermissions();
return super.getOneOrFail();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const result = await super.getOneOrFail();
const formattedResult = formatResult<T>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return formattedResult[0];
}
override getCount(): Promise<number> {
@@ -98,10 +174,25 @@ export class WorkspaceSelectQueryBuilder<
);
}
override getManyAndCount(): Promise<[T[], number]> {
override async getManyAndCount(): Promise<[T[], number]> {
this.validatePermissions();
return super.getManyAndCount();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const [result, count] = await super.getManyAndCount();
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return [formattedResult, count];
}
override insert(): WorkspaceInsertQueryBuilder<T> {
@@ -112,6 +203,7 @@ export class WorkspaceSelectQueryBuilder<
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
}
@@ -133,6 +225,7 @@ export class WorkspaceSelectQueryBuilder<
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
}
@@ -144,6 +237,7 @@ export class WorkspaceSelectQueryBuilder<
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
}
@@ -155,6 +249,7 @@ export class WorkspaceSelectQueryBuilder<
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
}
@@ -166,6 +261,7 @@ export class WorkspaceSelectQueryBuilder<
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
}
@@ -184,4 +280,17 @@ export class WorkspaceSelectQueryBuilder<
this.shouldBypassPermissionChecks,
);
}
private getMainAliasTarget(): EntityTarget<T> {
const mainAliasTarget = this.expressionMap.mainAlias?.target;
if (!mainAliasTarget) {
throw new TwentyORMException(
'Main alias target is missing',
TwentyORMExceptionCode.MISSING_MAIN_ALIAS_TARGET,
);
}
return mainAliasTarget;
}
}
@@ -1,13 +1,26 @@
import { ObjectRecordsPermissions } from 'twenty-shared/types';
import { InsertQueryBuilder, ObjectLiteral, UpdateResult } from 'typeorm';
import {
EntityTarget,
InsertQueryBuilder,
ObjectLiteral,
UpdateResult,
} from 'typeorm';
import { SoftDeleteQueryBuilder } from 'typeorm/query-builder/SoftDeleteQueryBuilder';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.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 {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
import { WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-delete-query-builder';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
export class WorkspaceSoftDeleteQueryBuilder<
T extends ObjectLiteral,
@@ -15,17 +28,20 @@ export class WorkspaceSoftDeleteQueryBuilder<
private objectRecordsPermissions: ObjectRecordsPermissions;
private shouldBypassPermissionChecks: boolean;
private internalContext: WorkspaceInternalContext;
private authContext?: AuthContext;
constructor(
queryBuilder: SoftDeleteQueryBuilder<T>,
objectRecordsPermissions: ObjectRecordsPermissions,
internalContext: WorkspaceInternalContext,
shouldBypassPermissionChecks: boolean,
authContext?: AuthContext,
) {
super(queryBuilder);
this.objectRecordsPermissions = objectRecordsPermissions;
this.internalContext = internalContext;
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
this.authContext = authContext;
}
override clone(): this {
@@ -36,10 +52,11 @@ export class WorkspaceSoftDeleteQueryBuilder<
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
) as this;
}
override execute(): Promise<UpdateResult> {
override async execute(): Promise<UpdateResult> {
validateQueryIsPermittedOrThrow(
this.expressionMap,
this.objectRecordsPermissions,
@@ -47,22 +64,74 @@ export class WorkspaceSoftDeleteQueryBuilder<
this.shouldBypassPermissionChecks,
);
return super.execute();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const after = await super.execute();
const formattedAfter = formatResult<T[]>(
after.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.DELETED,
objectMetadataItem: objectMetadata,
workspaceId: this.internalContext.workspaceId,
entities: formattedAfter,
authContext: this.authContext,
});
return {
raw: after.raw,
generatedMaps: formattedAfter,
affected: after.affected,
};
}
override select(): WorkspaceSelectQueryBuilder<T> {
throw new Error('This builder cannot morph into a select builder');
throw new TwentyORMException(
'This builder cannot morph into a select builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override update(): WorkspaceUpdateQueryBuilder<T> {
throw new Error('This builder cannot morph into an update builder');
throw new TwentyORMException(
'This builder cannot morph into an update builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override insert(): InsertQueryBuilder<T> {
throw new Error('This builder cannot morph into an insert builder');
throw new TwentyORMException(
'This builder cannot morph into an insert builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override delete(): WorkspaceDeleteQueryBuilder<T> {
throw new Error('This builder cannot morph into a delete builder');
throw new TwentyORMException(
'This builder cannot morph into a delete builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
private getMainAliasTarget(): EntityTarget<T> {
const mainAliasTarget = this.expressionMap.mainAlias?.target;
if (!mainAliasTarget) {
throw new TwentyORMException(
'Main alias target is missing',
TwentyORMExceptionCode.MISSING_MAIN_ALIAS_TARGET,
);
}
return mainAliasTarget;
}
}
@@ -1,12 +1,27 @@
import { ObjectRecordsPermissions } from 'twenty-shared/types';
import { ObjectLiteral, UpdateQueryBuilder, UpdateResult } from 'typeorm';
import {
EntityTarget,
ObjectLiteral,
UpdateQueryBuilder,
UpdateResult,
} from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.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 {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
import { WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-delete-query-builder';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-soft-delete-query-builder';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
export class WorkspaceUpdateQueryBuilder<
T extends ObjectLiteral,
@@ -14,16 +29,19 @@ export class WorkspaceUpdateQueryBuilder<
private objectRecordsPermissions: ObjectRecordsPermissions;
private shouldBypassPermissionChecks: boolean;
private internalContext: WorkspaceInternalContext;
private authContext?: AuthContext;
constructor(
queryBuilder: UpdateQueryBuilder<T>,
objectRecordsPermissions: ObjectRecordsPermissions,
internalContext: WorkspaceInternalContext,
shouldBypassPermissionChecks: boolean,
authContext?: AuthContext,
) {
super(queryBuilder);
this.objectRecordsPermissions = objectRecordsPermissions;
this.internalContext = internalContext;
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
this.authContext = authContext;
}
override clone(): this {
@@ -34,10 +52,11 @@ export class WorkspaceUpdateQueryBuilder<
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
) as this;
}
override execute(): Promise<UpdateResult> {
override async execute(): Promise<UpdateResult> {
validateQueryIsPermittedOrThrow(
this.expressionMap,
this.objectRecordsPermissions,
@@ -45,22 +64,108 @@ export class WorkspaceUpdateQueryBuilder<
this.shouldBypassPermissionChecks,
);
return super.execute();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const beforeSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
beforeSelectQueryBuilder.expressionMap.wheres = this.expressionMap.wheres;
beforeSelectQueryBuilder.expressionMap.aliases = this.expressionMap.aliases;
beforeSelectQueryBuilder.setParameters(this.getParameters());
const before = await beforeSelectQueryBuilder.getMany();
const formattedBefore = formatResult<T[]>(
before,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const after = await super.execute();
const formattedAfter = formatResult<T[]>(
after.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.UPDATED,
objectMetadataItem: objectMetadata,
workspaceId: this.internalContext.workspaceId,
entities: formattedAfter,
beforeEntities: formattedBefore,
authContext: this.authContext,
});
return {
raw: after.raw,
generatedMaps: formattedAfter,
affected: after.affected,
};
}
override set(_values: QueryDeepPartialEntity<T>): this {
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const formattedUpdateSet = formatData(_values, objectMetadata);
return super.set(formattedUpdateSet);
}
override select(): WorkspaceSelectQueryBuilder<T> {
throw new Error('This builder cannot morph into a select builder');
throw new TwentyORMException(
'This builder cannot morph into a select builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override delete(): WorkspaceDeleteQueryBuilder<T> {
throw new Error('This builder cannot morph into a delete builder');
throw new TwentyORMException(
'This builder cannot morph into a delete builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override softDelete(): WorkspaceSoftDeleteQueryBuilder<T> {
throw new Error('This builder cannot morph into a soft delete builder');
throw new TwentyORMException(
'This builder cannot morph into a soft delete builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
override restore(): WorkspaceSoftDeleteQueryBuilder<T> {
throw new Error('This builder cannot morph into a soft delete builder');
throw new TwentyORMException(
'This builder cannot morph into a soft delete builder',
TwentyORMExceptionCode.METHOD_NOT_ALLOWED,
);
}
private getMainAliasTarget(): EntityTarget<T> {
const mainAliasTarget = this.expressionMap.mainAlias?.target;
if (!mainAliasTarget) {
throw new TwentyORMException(
'Main alias target is missing',
TwentyORMExceptionCode.MISSING_MAIN_ALIAS_TARGET,
);
}
return mainAliasTarget;
}
}
@@ -22,16 +22,15 @@ import { UpsertOptions } from 'typeorm/repository/UpsertOptions';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { QueryDeepPartialEntityWithRelationConnect } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
export class WorkspaceRepository<
@@ -41,6 +40,7 @@ export class WorkspaceRepository<
private shouldBypassPermissionChecks: boolean;
private featureFlagMap: FeatureFlagMap;
private objectRecordsPermissions?: ObjectRecordsPermissions;
private authContext?: AuthContext;
declare manager: WorkspaceEntityManager;
constructor(
@@ -51,6 +51,7 @@ export class WorkspaceRepository<
queryRunner?: QueryRunner,
objectRecordsPermissions?: ObjectRecordsPermissions,
shouldBypassPermissionChecks = false,
authContext?: AuthContext,
) {
super(target, manager, queryRunner);
this.internalContext = internalContext;
@@ -58,6 +59,7 @@ export class WorkspaceRepository<
this.objectRecordsPermissions = objectRecordsPermissions;
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
this.manager = manager;
this.authContext = authContext;
}
override createQueryBuilder<U extends T>(
@@ -78,6 +80,7 @@ export class WorkspaceRepository<
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
}
@@ -99,9 +102,8 @@ export class WorkspaceRepository<
computedOptions,
permissionOptions,
);
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
override async findBy(
@@ -119,9 +121,8 @@ export class WorkspaceRepository<
computedOptions.where,
permissionOptions,
);
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
override async findAndCount(
@@ -139,9 +140,8 @@ export class WorkspaceRepository<
computedOptions,
permissionOptions,
);
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
override async findAndCountBy(
@@ -159,9 +159,8 @@ export class WorkspaceRepository<
computedOptions.where,
permissionOptions,
);
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
override async findOne(
@@ -179,9 +178,8 @@ export class WorkspaceRepository<
computedOptions,
permissionOptions,
);
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
override async findOneBy(
@@ -199,9 +197,8 @@ export class WorkspaceRepository<
computedOptions.where,
permissionOptions,
);
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
override async findOneOrFail(
@@ -219,9 +216,8 @@ export class WorkspaceRepository<
computedOptions,
permissionOptions,
);
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
override async findOneByOrFail(
@@ -239,9 +235,8 @@ export class WorkspaceRepository<
computedOptions.where,
permissionOptions,
);
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
/**
@@ -277,7 +272,6 @@ export class WorkspaceRepository<
entityManager?: WorkspaceEntityManager,
): Promise<U | U[]> {
const manager = entityManager || this.manager;
const formattedEntityOrEntities = await this.formatData(entityOrEntities);
let result: U | U[];
const permissionOptions = {
@@ -286,25 +280,23 @@ export class WorkspaceRepository<
};
// Needed because save method has multiple signature, otherwise we will need to do a type assertion
if (Array.isArray(formattedEntityOrEntities)) {
if (Array.isArray(entityOrEntities)) {
result = await manager.save(
this.target,
formattedEntityOrEntities,
entityOrEntities,
options,
permissionOptions,
);
} else {
result = await manager.save(
this.target,
formattedEntityOrEntities,
entityOrEntities,
options,
permissionOptions,
);
}
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
/**
@@ -328,21 +320,18 @@ export class WorkspaceRepository<
entityManager?: WorkspaceEntityManager,
): Promise<T | T[]> {
const manager = entityManager || this.manager;
const formattedEntityOrEntities = await this.formatData(entityOrEntities);
const permissionOptions = {
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
objectRecordsPermissions: this.objectRecordsPermissions,
};
const result = await manager.remove(
this.target,
formattedEntityOrEntities,
entityOrEntities,
options,
permissionOptions,
);
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
override async delete(
@@ -402,7 +391,6 @@ export class WorkspaceRepository<
entityManager?: WorkspaceEntityManager,
): Promise<U | U[]> {
const manager = entityManager || this.manager;
const formattedEntityOrEntities = await this.formatData(entityOrEntities);
const permissionOptions = {
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
objectRecordsPermissions: this.objectRecordsPermissions,
@@ -410,25 +398,23 @@ export class WorkspaceRepository<
let result: U | U[];
// Needed because save method has multiple signature, otherwise we will need to do a type assertion
if (Array.isArray(formattedEntityOrEntities)) {
if (Array.isArray(entityOrEntities)) {
result = await manager.softRemove(
this.target,
formattedEntityOrEntities,
entityOrEntities,
options,
permissionOptions,
);
} else {
result = await manager.softRemove(
this.target,
formattedEntityOrEntities,
entityOrEntities,
options,
permissionOptions,
);
}
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
override async softDelete(
@@ -491,7 +477,6 @@ export class WorkspaceRepository<
entityManager?: WorkspaceEntityManager,
): Promise<U | U[]> {
const manager = entityManager || this.manager;
const formattedEntityOrEntities = await this.formatData(entityOrEntities);
const permissionOptions = {
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
objectRecordsPermissions: this.objectRecordsPermissions,
@@ -499,25 +484,23 @@ export class WorkspaceRepository<
let result: U | U[];
// Needed because save method has multiple signature, otherwise we will need to do a type assertion
if (Array.isArray(formattedEntityOrEntities)) {
if (Array.isArray(entityOrEntities)) {
result = await manager.recover(
this.target,
formattedEntityOrEntities,
entityOrEntities,
options,
permissionOptions,
);
} else {
result = await manager.recover(
this.target,
formattedEntityOrEntities,
entityOrEntities,
options,
permissionOptions,
);
}
const formattedResult = await this.formatResult(result);
return formattedResult;
return result;
}
override async restore(
@@ -558,23 +541,12 @@ export class WorkspaceRepository<
): Promise<InsertResult> {
const manager = entityManager || this.manager;
const formattedEntity = await this.formatData(entity);
const permissionOptions = {
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
objectRecordsPermissions: this.objectRecordsPermissions,
};
const result = await manager.insert(
this.target,
formattedEntity,
permissionOptions,
);
const formattedResult = await this.formatResult(result.generatedMaps);
return {
raw: result.raw,
generatedMaps: formattedResult,
identifiers: result.identifiers,
};
return manager.insert(this.target, entity, permissionOptions);
}
/**
@@ -620,8 +592,6 @@ export class WorkspaceRepository<
): Promise<InsertResult> {
const manager = entityManager || this.manager;
const formattedEntityOrEntities = await this.formatData(entityOrEntities);
const permissionOptions = {
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
objectRecordsPermissions: this.objectRecordsPermissions,
@@ -629,16 +599,14 @@ export class WorkspaceRepository<
const result = await manager.upsert(
this.target,
formattedEntityOrEntities,
entityOrEntities,
conflictPathsOrOptions,
permissionOptions,
);
const formattedResult = await this.formatResult(result.generatedMaps);
return {
raw: result.raw,
generatedMaps: formattedResult,
generatedMaps: result.generatedMaps,
identifiers: result.identifiers,
};
}
@@ -862,13 +830,12 @@ export class WorkspaceRepository<
entityManager?: WorkspaceEntityManager,
): Promise<T | undefined> {
const manager = entityManager || this.manager;
const formattedEntityLike = await this.formatData(entityLike);
const permissionOptions = {
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
objectRecordsPermissions: this.objectRecordsPermissions,
};
return manager.preload(this.target, formattedEntityLike, permissionOptions);
return manager.preload(this.target, entityLike, permissionOptions);
}
/**
@@ -940,15 +907,4 @@ export class WorkspaceRepository<
return formatData(data, objectMetadata) as T;
}
async formatResult<T>(
data: T,
objectMetadata?: ObjectMetadataItemWithFieldMaps,
): Promise<T> {
objectMetadata ??= await this.getObjectMetadataFromTarget();
const objectMetadataMaps = this.internalContext.objectMetadataMaps;
return formatResult(data, objectMetadata, objectMetadataMaps) as T;
}
}