Add TwentyORM query read timeout exception (#13603)

In this PR:
- adding a try / catch around all ORM internal methods save, insert,
upsert, findOne, ...
- leveraging this error to prevent messageChannels to get FAILED
- optimizing messaging BATCH_SIZE and THROTTLE threshold according to
local tests
- 

<img width="1510" height="851" alt="image"
src="https://github.com/user-attachments/assets/802fd933-caac-4291-9cde-34a1ddf59c06"
/>
This commit is contained in:
Charles Bochet
2025-08-04 17:16:16 +02:00
committed by GitHub
parent d958447bb6
commit 2eeddcddc6
14 changed files with 656 additions and 553 deletions
@@ -54,6 +54,7 @@ import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.
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';
import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception';
type PermissionOptions = {
shouldBypassPermissionChecks?: boolean;
@@ -1057,165 +1058,170 @@ export class WorkspaceEntityManager extends EntityManager {
| (SaveOptions & { reload: false }),
permissionOptions?: PermissionOptions,
): Promise<(T & Entity) | (T & Entity)[] | Entity | Entity[]> {
const permissionOptionsFromArgs =
maybeOptionsOrMaybePermissionOptions &&
('shouldBypassPermissionChecks' in maybeOptionsOrMaybePermissionOptions ||
'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions)
try {
const permissionOptionsFromArgs =
maybeOptionsOrMaybePermissionOptions &&
('shouldBypassPermissionChecks' in
maybeOptionsOrMaybePermissionOptions ||
'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions)
? maybeOptionsOrMaybePermissionOptions
: permissionOptions;
let target =
arguments.length > 1 &&
(typeof targetOrEntity === 'function' ||
InstanceChecker.isEntitySchema(targetOrEntity) ||
typeof targetOrEntity === 'string')
? targetOrEntity
: undefined;
const entity = target ? entityOrMaybeOptions : targetOrEntity;
const options = target
? maybeOptionsOrMaybePermissionOptions
: permissionOptions;
: entityOrMaybeOptions;
let target =
arguments.length > 1 &&
(typeof targetOrEntity === 'function' ||
InstanceChecker.isEntitySchema(targetOrEntity) ||
typeof targetOrEntity === 'string')
? targetOrEntity
: undefined;
if (InstanceChecker.isEntitySchema(target)) target = target.options.name;
if (Array.isArray(entity) && entity.length === 0)
return Promise.resolve(entity as Entity[]);
const entity = target ? entityOrMaybeOptions : targetOrEntity;
const queryRunnerForEntityPersistExecutor =
this.connection.createQueryRunnerForEntityPersistExecutor();
const options = target
? maybeOptionsOrMaybePermissionOptions
: entityOrMaybeOptions;
const isEntityArray = Array.isArray(entity);
const entityTarget =
target ?? (isEntityArray ? entity[0]?.constructor : entity.constructor);
if (InstanceChecker.isEntitySchema(target)) target = target.options.name;
if (Array.isArray(entity) && entity.length === 0)
return Promise.resolve(entity as Entity[]);
const entityArray = isEntityArray ? entity : [entity];
const queryRunnerForEntityPersistExecutor =
this.connection.createQueryRunnerForEntityPersistExecutor();
const isEntityArray = Array.isArray(entity);
const entityTarget =
target ?? (isEntityArray ? entity[0]?.constructor : entity.constructor);
const entityArray = isEntityArray ? entity : [entity];
const relationNestedQueries = new RelationNestedQueries(
this.internalContext,
);
const relationNestedConfig =
relationNestedQueries.prepareNestedRelationQueries(
entityArray,
entityTarget,
const relationNestedQueries = new RelationNestedQueries(
this.internalContext,
);
const entityWithConnectedRelations = isDefined(relationNestedConfig)
? await relationNestedQueries.processRelationNestedQueries({
entities: entityArray,
relationNestedConfig,
queryBuilder: this.createQueryBuilder(
undefined,
undefined,
undefined,
permissionOptions,
),
})
: entityArray;
const relationNestedConfig =
relationNestedQueries.prepareNestedRelationQueries(
entityArray,
entityTarget,
);
const entityIds = entityArray
.map((entity) => (entity as { id: string }).id)
.filter(isDefined);
const beforeUpdate = await this.find(
entityTarget,
{
where: { id: In(entityIds) },
},
{ shouldBypassPermissionChecks: true }, // Bypass as this is for event emission
);
const entityWithConnectedRelations = isDefined(relationNestedConfig)
? await relationNestedQueries.processRelationNestedQueries({
entities: entityArray,
relationNestedConfig,
queryBuilder: this.createQueryBuilder(
undefined,
undefined,
undefined,
permissionOptions,
),
})
: entityArray;
const beforeUpdateMapById = beforeUpdate.reduce(
(acc, e: ObjectLiteral) => {
acc[e.id] = e;
const entityIds = entityArray
.map((entity) => (entity as { id: string }).id)
.filter(isDefined);
const beforeUpdate = await this.find(
entityTarget,
{
where: { id: In(entityIds) },
},
{ shouldBypassPermissionChecks: true }, // Bypass as this is for event emission
);
return acc;
},
{} as Record<string, ObjectLiteral>,
);
const beforeUpdateMapById = beforeUpdate.reduce(
(acc, e: ObjectLiteral) => {
acc[e.id] = e;
const objectMetadataItem = getObjectMetadataFromEntityTarget(
entityTarget,
this.internalContext,
);
return acc;
},
{} as Record<string, ObjectLiteral>,
);
const formattedEntityOrEntities = formatData(
entityWithConnectedRelations,
objectMetadataItem,
);
const objectMetadataItem = getObjectMetadataFromEntityTarget(
entityTarget,
this.internalContext,
);
const updatedColumns = formattedEntityOrEntities
.map((e) => Object.keys(e))
.flat();
this.validatePermissions({
target: targetOrEntity,
operationType: 'update',
permissionOptions: permissionOptionsFromArgs,
selectedColumns: [],
updatedColumns,
});
const result = await new EntityPersistExecutor(
this.connection,
queryRunnerForEntityPersistExecutor,
'save',
target,
formattedEntityOrEntities as ObjectLiteral[],
options as SaveOptions | (SaveOptions & { reload: false }),
)
.execute()
.then(() => formattedEntityOrEntities as Entity[])
.finally(() => queryRunnerForEntityPersistExecutor.release());
const resultArray = Array.isArray(result) ? result : [result];
let formattedResult = formatResult<Entity[]>(
resultArray,
objectMetadataItem,
this.internalContext.objectMetadataMaps,
);
const updatedEntities = formattedResult.filter(
(entity) => beforeUpdateMapById[entity.id],
);
const createdEntities = formattedResult.filter(
(entity) => !beforeUpdateMapById[entity.id],
);
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.UPDATED,
objectMetadataItem,
workspaceId: this.internalContext.workspaceId,
entities: updatedEntities,
beforeEntities: updatedEntities.map(
(entity) => beforeUpdateMapById[entity.id],
),
});
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.CREATED,
objectMetadataItem,
workspaceId: this.internalContext.workspaceId,
entities: createdEntities,
});
const isFieldPermissionsEnabled =
this.getFeatureFlagMap().IS_FIELDS_PERMISSIONS_ENABLED;
const permissionCheckApplies =
permissionOptionsFromArgs?.shouldBypassPermissionChecks !== true &&
objectMetadataItem.isSystem !== true;
if (isFieldPermissionsEnabled && permissionCheckApplies) {
formattedResult = this.getFormattedResultWithoutNonReadableFields({
formattedResult,
const formattedEntityOrEntities = formatData(
entityWithConnectedRelations,
objectMetadataItem,
permissionOptionsFromArgs,
});
}
);
return isEntityArray ? formattedResult : formattedResult[0];
const updatedColumns = formattedEntityOrEntities
.map((e) => Object.keys(e))
.flat();
this.validatePermissions({
target: targetOrEntity,
operationType: 'update',
permissionOptions: permissionOptionsFromArgs,
selectedColumns: [],
updatedColumns,
});
const result = await new EntityPersistExecutor(
this.connection,
queryRunnerForEntityPersistExecutor,
'save',
target,
formattedEntityOrEntities as ObjectLiteral[],
options as SaveOptions | (SaveOptions & { reload: false }),
)
.execute()
.then(() => formattedEntityOrEntities as Entity[])
.finally(() => queryRunnerForEntityPersistExecutor.release());
const resultArray = Array.isArray(result) ? result : [result];
let formattedResult = formatResult<Entity[]>(
resultArray,
objectMetadataItem,
this.internalContext.objectMetadataMaps,
);
const updatedEntities = formattedResult.filter(
(entity) => beforeUpdateMapById[entity.id],
);
const createdEntities = formattedResult.filter(
(entity) => !beforeUpdateMapById[entity.id],
);
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.UPDATED,
objectMetadataItem,
workspaceId: this.internalContext.workspaceId,
entities: updatedEntities,
beforeEntities: updatedEntities.map(
(entity) => beforeUpdateMapById[entity.id],
),
});
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.CREATED,
objectMetadataItem,
workspaceId: this.internalContext.workspaceId,
entities: createdEntities,
});
const isFieldPermissionsEnabled =
this.getFeatureFlagMap().IS_FIELDS_PERMISSIONS_ENABLED;
const permissionCheckApplies =
permissionOptionsFromArgs?.shouldBypassPermissionChecks !== true &&
objectMetadataItem.isSystem !== true;
if (isFieldPermissionsEnabled && permissionCheckApplies) {
formattedResult = this.getFormattedResultWithoutNonReadableFields({
formattedResult,
objectMetadataItem,
permissionOptionsFromArgs,
});
}
return isEntityArray ? formattedResult : formattedResult[0];
} catch (error) {
throw computeTwentyORMException(error);
}
}
private getFormattedResultWithoutNonReadableFields<
@@ -0,0 +1,23 @@
import { t } from '@lingui/core/macro';
import { QueryFailedError } from 'typeorm';
import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
export const computeTwentyORMException = (error: Error) => {
if (error instanceof QueryFailedError) {
if (error.message.includes('Query read timeout')) {
return new TwentyORMException(
'Query read timeout',
TwentyORMExceptionCode.QUERY_READ_TIMEOUT,
{
userFriendlyMessage: t`We are experiencing a temporary issue with our database. Please try again later.`,
},
);
}
}
return error;
};
@@ -17,4 +17,5 @@ export enum TwentyORMExceptionCode {
MISSING_MAIN_ALIAS_TARGET = 'MISSING_MAIN_ALIAS_TARGET',
METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED',
ENUM_TYPE_NAME_NOT_FOUND = 'ENUM_TYPE_NAME_NOT_FOUND',
QUERY_READ_TIMEOUT = 'QUERY_READ_TIMEOUT',
}
@@ -14,6 +14,7 @@ import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/works
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 { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception';
import {
TwentyORMException,
TwentyORMExceptionCode,
@@ -62,64 +63,69 @@ export class WorkspaceDeleteQueryBuilder<
}
override async execute(): Promise<DeleteResult & { generatedMaps: T[] }> {
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectRecordsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
try {
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectRecordsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
const mainAliasTarget = this.getMainAliasTarget();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const eventSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
true,
this.authContext,
this.featureFlagMap,
);
const eventSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
true,
this.authContext,
this.featureFlagMap,
);
eventSelectQueryBuilder.expressionMap.wheres = this.expressionMap.wheres;
eventSelectQueryBuilder.expressionMap.aliases = this.expressionMap.aliases;
eventSelectQueryBuilder.setParameters(this.getParameters());
eventSelectQueryBuilder.expressionMap.wheres = this.expressionMap.wheres;
eventSelectQueryBuilder.expressionMap.aliases =
this.expressionMap.aliases;
eventSelectQueryBuilder.setParameters(this.getParameters());
const before = await eventSelectQueryBuilder.getOne();
const before = await eventSelectQueryBuilder.getOne();
const result = await super.execute();
const result = await super.execute();
const formattedResult = formatResult<T[]>(
result.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const formattedResult = formatResult<T[]>(
result.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const formattedBefore = formatResult<T[]>(
before,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const formattedBefore = formatResult<T[]>(
before,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.DESTROYED,
objectMetadataItem: objectMetadata,
workspaceId: this.internalContext.workspaceId,
entities: formattedBefore,
authContext: this.authContext,
});
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.DESTROYED,
objectMetadataItem: objectMetadata,
workspaceId: this.internalContext.workspaceId,
entities: formattedBefore,
authContext: this.authContext,
});
return {
raw: result.raw,
generatedMaps: formattedResult,
affected: result.affected,
};
return {
raw: result.raw,
generatedMaps: formattedResult,
affected: result.affected,
};
} catch (error) {
throw computeTwentyORMException(error);
}
}
private getMainAliasTarget(): EntityTarget<T> {
@@ -17,6 +17,7 @@ import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twent
import { RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type';
import { RelationDisconnectQueryFieldsByEntityIndex } from 'src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception';
import {
TwentyORMException,
TwentyORMExceptionCode,
@@ -100,98 +101,102 @@ export class WorkspaceInsertQueryBuilder<
}
override async execute(): Promise<InsertResult> {
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectRecordsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
try {
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectRecordsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
const mainAliasTarget = this.getMainAliasTarget();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
if (isDefined(this.relationNestedConfig)) {
const nestedRelationQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
const updatedValues =
await this.relationNestedQueries.processRelationNestedQueries({
entities: this.expressionMap.valuesSet as
| QueryDeepPartialEntityWithNestedRelationFields<T>
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
relationNestedConfig: this.relationNestedConfig,
queryBuilder: nestedRelationQueryBuilder,
});
if (isDefined(this.relationNestedConfig)) {
const nestedRelationQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
this.expressionMap.valuesSet = updatedValues;
const updatedValues =
await this.relationNestedQueries.processRelationNestedQueries({
entities: this.expressionMap.valuesSet as
| QueryDeepPartialEntityWithNestedRelationFields<T>
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
relationNestedConfig: this.relationNestedConfig,
queryBuilder: nestedRelationQueryBuilder,
});
this.expressionMap.valuesSet = updatedValues;
}
const result = await super.execute();
const eventSelectQueryBuilder = (
this.connection.manager as WorkspaceEntityManager
).createQueryBuilder(
mainAliasTarget,
this.expressionMap.mainAlias?.metadata.name ?? '',
undefined,
{
shouldBypassPermissionChecks: true,
},
) as WorkspaceSelectQueryBuilder<T>;
eventSelectQueryBuilder.whereInIds(
result.identifiers.map((identifier) => identifier.id),
);
const afterResult = await eventSelectQueryBuilder.getMany();
const formattedResultForEvent = formatResult<T[]>(
afterResult,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.CREATED,
objectMetadataItem: objectMetadata,
workspaceId: this.internalContext.workspaceId,
entities: formattedResultForEvent,
authContext: this.authContext,
});
// TypeORM returns all entity columns for insertions
const resultWithoutInsertionExtraColumns = result.raw.map(
(rawResult: Record<string, string>) =>
Object.keys(rawResult)
.filter((key) => this.expressionMap.returning.includes(key))
.reduce((filtered: Record<string, string>, key) => {
filtered[key] = rawResult[key];
return filtered;
}, {}),
);
const formattedResult = formatResult<T[]>(
resultWithoutInsertionExtraColumns,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return {
raw: resultWithoutInsertionExtraColumns,
generatedMaps: formattedResult,
identifiers: result.identifiers,
};
} catch (error) {
throw computeTwentyORMException(error);
}
const result = await super.execute();
const eventSelectQueryBuilder = (
this.connection.manager as WorkspaceEntityManager
).createQueryBuilder(
mainAliasTarget,
this.expressionMap.mainAlias?.metadata.name ?? '',
undefined,
{
shouldBypassPermissionChecks: true,
},
) as WorkspaceSelectQueryBuilder<T>;
eventSelectQueryBuilder.whereInIds(
result.identifiers.map((identifier) => identifier.id),
);
const afterResult = await eventSelectQueryBuilder.getMany();
const formattedResultForEvent = formatResult<T[]>(
afterResult,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.CREATED,
objectMetadataItem: objectMetadata,
workspaceId: this.internalContext.workspaceId,
entities: formattedResultForEvent,
authContext: this.authContext,
});
// TypeORM returns all entity columns for insertions
const resultWithoutInsertionExtraColumns = result.raw.map(
(rawResult: Record<string, string>) =>
Object.keys(rawResult)
.filter((key) => this.expressionMap.returning.includes(key))
.reduce((filtered: Record<string, string>, key) => {
filtered[key] = rawResult[key];
return filtered;
}, {}),
);
const formattedResult = formatResult<T[]>(
resultWithoutInsertionExtraColumns,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return {
raw: resultWithoutInsertionExtraColumns,
generatedMaps: formattedResult,
identifiers: result.identifiers,
};
}
private getMainAliasTarget(): EntityTarget<T> {
@@ -11,6 +11,7 @@ import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception';
import {
TwentyORMException,
TwentyORMExceptionCode,
@@ -66,111 +67,139 @@ export class WorkspaceSelectQueryBuilder<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override async execute(): Promise<any> {
this.validatePermissions();
try {
this.validatePermissions();
const mainAliasTarget = this.getMainAliasTarget();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const result = await super.execute();
const result = await super.execute();
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return {
raw: result,
generatedMaps: formattedResult,
identifiers: result.identifiers,
};
return {
raw: result,
generatedMaps: formattedResult,
identifiers: result.identifiers,
};
} catch (error) {
throw computeTwentyORMException(error);
}
}
override async getMany(): Promise<T[]> {
this.validatePermissions();
try {
this.validatePermissions();
const mainAliasTarget = this.getMainAliasTarget();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const result = await super.getMany();
const result = await super.getMany();
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return formattedResult;
return formattedResult;
} catch (error) {
throw computeTwentyORMException(error);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override getRawOne<U = any>(): Promise<U | undefined> {
this.validatePermissions();
try {
this.validatePermissions();
return super.getRawOne();
return super.getRawOne();
} catch (error) {
throw computeTwentyORMException(error);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override getRawMany<U = any>(): Promise<U[]> {
this.validatePermissions();
try {
this.validatePermissions();
return super.getRawMany();
return super.getRawMany();
} catch (error) {
throw computeTwentyORMException(error);
}
}
override async getOne(): Promise<T | null> {
this.validatePermissions();
try {
this.validatePermissions();
const mainAliasTarget = this.getMainAliasTarget();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const result = await super.getOne();
const result = await super.getOne();
const formattedResult = formatResult<T>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const formattedResult = formatResult<T>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return formattedResult;
return formattedResult;
} catch (error) {
throw computeTwentyORMException(error);
}
}
override async getOneOrFail(): Promise<T> {
this.validatePermissions();
try {
this.validatePermissions();
const mainAliasTarget = this.getMainAliasTarget();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const result = await super.getOneOrFail();
const result = await super.getOneOrFail();
const formattedResult = formatResult<T>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const formattedResult = formatResult<T>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return formattedResult[0];
return formattedResult[0];
} catch (error) {
throw computeTwentyORMException(error);
}
}
override getCount(): Promise<number> {
this.validatePermissions();
try {
this.validatePermissions();
return super.getCount();
return super.getCount();
} catch (error) {
throw computeTwentyORMException(error);
}
}
override getExists(): Promise<boolean> {
@@ -181,24 +210,28 @@ export class WorkspaceSelectQueryBuilder<
}
override async getManyAndCount(): Promise<[T[], number]> {
this.validatePermissions();
try {
this.validatePermissions();
const mainAliasTarget = this.getMainAliasTarget();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const [result, count] = await super.getManyAndCount();
const [result, count] = await super.getManyAndCount();
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return [formattedResult, count];
return [formattedResult, count];
} catch (error) {
throw computeTwentyORMException(error);
}
}
override insert(): WorkspaceInsertQueryBuilder<T> {
@@ -13,6 +13,7 @@ import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/works
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 { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception';
import {
TwentyORMException,
TwentyORMExceptionCode,
@@ -62,43 +63,47 @@ export class WorkspaceSoftDeleteQueryBuilder<
}
override async execute(): Promise<UpdateResult> {
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectRecordsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
try {
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectRecordsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
const mainAliasTarget = this.getMainAliasTarget();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const after = await super.execute();
const after = await super.execute();
const formattedAfter = formatResult<T[]>(
after.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
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,
});
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,
};
return {
raw: after.raw,
generatedMaps: formattedAfter,
affected: after.affected,
};
} catch (error) {
throw computeTwentyORMException(error);
}
}
override select(): WorkspaceSelectQueryBuilder<T> {
@@ -17,6 +17,7 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu
import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type';
import { RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type';
import { RelationDisconnectQueryFieldsByEntityIndex } from 'src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type';
import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception';
import {
TwentyORMException,
TwentyORMExceptionCode,
@@ -80,156 +81,42 @@ export class WorkspaceUpdateQueryBuilder<
}
override async execute(): Promise<UpdateResult> {
if (this.manyInputs) {
return this.executeMany();
}
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectRecordsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const eventSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
true,
this.authContext,
this.featureFlagMap,
);
eventSelectQueryBuilder.expressionMap.wheres = this.expressionMap.wheres;
eventSelectQueryBuilder.expressionMap.aliases = this.expressionMap.aliases;
eventSelectQueryBuilder.setParameters(this.getParameters());
const before = await eventSelectQueryBuilder.getMany();
const nestedRelationQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
if (isDefined(this.relationNestedConfig)) {
const updatedValues =
await this.relationNestedQueries.processRelationNestedQueries({
entities: this.expressionMap.valuesSet as
| QueryDeepPartialEntityWithNestedRelationFields<T>
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
relationNestedConfig: this.relationNestedConfig,
queryBuilder: nestedRelationQueryBuilder,
});
this.expressionMap.valuesSet =
updatedValues.length === 1 ? updatedValues[0] : updatedValues;
}
const formattedBefore = formatResult<T[]>(
before,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const result = await super.execute();
const after = await eventSelectQueryBuilder.getMany();
const formattedAfter = formatResult<T[]>(
after,
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,
});
const formattedResult = formatResult<T[]>(
result.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return {
raw: result.raw,
generatedMaps: formattedResult,
affected: result.affected,
};
}
public async executeMany(): Promise<UpdateResult> {
for (const input of this.manyInputs) {
const fakeExpressionMapToValidatePermissions = Object.assign(
{},
this.expressionMap,
{
wheres: input.criteria,
valuesSet: input.partialEntity,
},
);
try {
if (this.manyInputs) {
return this.executeMany();
}
validateQueryIsPermittedOrThrow({
expressionMap: fakeExpressionMapToValidatePermissions,
expressionMap: this.expressionMap,
objectRecordsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
}
const mainAliasTarget = this.getMainAliasTarget();
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const eventSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
true,
this.authContext,
this.featureFlagMap,
);
const eventSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
true,
this.authContext,
this.featureFlagMap,
);
eventSelectQueryBuilder.whereInIds(
this.manyInputs.map((input) => input.criteria),
);
eventSelectQueryBuilder.expressionMap.aliases = this.expressionMap.aliases;
eventSelectQueryBuilder.setParameters(this.getParameters());
eventSelectQueryBuilder.expressionMap.wheres = this.expressionMap.wheres;
eventSelectQueryBuilder.expressionMap.aliases =
this.expressionMap.aliases;
eventSelectQueryBuilder.setParameters(this.getParameters());
const beforeRecords = await eventSelectQueryBuilder.getMany();
const formattedBefore = formatResult<T[]>(
beforeRecords,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const results: UpdateResult[] = [];
for (const input of this.manyInputs) {
this.expressionMap.valuesSet = input.partialEntity;
this.where({ id: input.criteria });
const before = await eventSelectQueryBuilder.getMany();
const nestedRelationQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
@@ -242,7 +129,7 @@ export class WorkspaceUpdateQueryBuilder<
if (isDefined(this.relationNestedConfig)) {
const updatedValues =
await this.relationNestedQueries.processRelationNestedQueries({
entities: input.partialEntity as
entities: this.expressionMap.valuesSet as
| QueryDeepPartialEntityWithNestedRelationFields<T>
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
relationNestedConfig: this.relationNestedConfig,
@@ -253,39 +140,163 @@ export class WorkspaceUpdateQueryBuilder<
updatedValues.length === 1 ? updatedValues[0] : updatedValues;
}
const formattedBefore = formatResult<T[]>(
before,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const result = await super.execute();
const after = await eventSelectQueryBuilder.getMany();
results.push(result);
const formattedAfter = formatResult<T[]>(
after,
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,
});
const formattedResult = formatResult<T[]>(
result.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return {
raw: result.raw,
generatedMaps: formattedResult,
affected: result.affected,
};
} catch (error) {
throw computeTwentyORMException(error);
}
}
const afterRecords = await eventSelectQueryBuilder.getMany();
public async executeMany(): Promise<UpdateResult> {
try {
for (const input of this.manyInputs) {
const fakeExpressionMapToValidatePermissions = Object.assign(
{},
this.expressionMap,
{
wheres: input.criteria,
valuesSet: input.partialEntity,
},
);
const formattedAfter = formatResult<T[]>(
afterRecords,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
validateQueryIsPermittedOrThrow({
expressionMap: fakeExpressionMapToValidatePermissions,
objectRecordsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
}
await this.internalContext.eventEmitterService.emitMutationEvent({
action: DatabaseEventAction.UPDATED,
objectMetadataItem: objectMetadata,
workspaceId: this.internalContext.workspaceId,
entities: formattedAfter,
beforeEntities: formattedBefore,
authContext: this.authContext,
});
const mainAliasTarget = this.getMainAliasTarget();
const formattedResults = formatResult<T[]>(
results.map((result) => result.raw),
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
return {
raw: results.map((result) => result.raw),
generatedMaps: formattedResults,
affected: results.length,
};
const eventSelectQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
true,
this.authContext,
this.featureFlagMap,
);
eventSelectQueryBuilder.whereInIds(
this.manyInputs.map((input) => input.criteria),
);
eventSelectQueryBuilder.expressionMap.aliases =
this.expressionMap.aliases;
eventSelectQueryBuilder.setParameters(this.getParameters());
const beforeRecords = await eventSelectQueryBuilder.getMany();
const formattedBefore = formatResult<T[]>(
beforeRecords,
objectMetadata,
this.internalContext.objectMetadataMaps,
);
const results: UpdateResult[] = [];
for (const input of this.manyInputs) {
this.expressionMap.valuesSet = input.partialEntity;
this.where({ id: input.criteria });
const nestedRelationQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
this.authContext,
);
if (isDefined(this.relationNestedConfig)) {
const updatedValues =
await this.relationNestedQueries.processRelationNestedQueries({
entities: input.partialEntity as
| QueryDeepPartialEntityWithNestedRelationFields<T>
| QueryDeepPartialEntityWithNestedRelationFields<T>[],
relationNestedConfig: this.relationNestedConfig,
queryBuilder: nestedRelationQueryBuilder,
});
this.expressionMap.valuesSet =
updatedValues.length === 1 ? updatedValues[0] : updatedValues;
}
const result = await super.execute();
results.push(result);
}
const afterRecords = await eventSelectQueryBuilder.getMany();
const formattedAfter = formatResult<T[]>(
afterRecords,
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,
});
const formattedResults = formatResult<T[]>(
results.map((result) => result.raw),
objectMetadata,
this.internalContext.objectMetadataMaps,
);
return {
raw: results.map((result) => result.raw),
generatedMaps: formattedResults,
affected: results.length,
};
} catch (error) {
throw computeTwentyORMException(error);
}
}
override set(
@@ -1 +1 @@
export const CALENDAR_THROTTLE_MAX_ATTEMPTS = 4;
export const CALENDAR_THROTTLE_MAX_ATTEMPTS = 5;
@@ -1,6 +1,10 @@
import { Injectable, Logger } from '@nestjs/common';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { CALENDAR_THROTTLE_MAX_ATTEMPTS } from 'src/modules/calendar/calendar-event-import-manager/constants/calendar-throttle-max-attempts';
import {
@@ -31,7 +35,7 @@ export class CalendarEventImportErrorHandlerService {
) {}
public async handleDriverException(
exception: CalendarEventImportDriverException,
exception: CalendarEventImportDriverException | TwentyORMException,
syncStep: CalendarEventImportSyncStep,
calendarChannel: Pick<
CalendarChannelWorkspaceEntity,
@@ -47,6 +51,7 @@ export class CalendarEventImportErrorHandlerService {
workspaceId,
);
break;
case TwentyORMExceptionCode.QUERY_READ_TIMEOUT:
case CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR:
await this.handleTemporaryException(
syncStep,
@@ -158,7 +163,7 @@ export class CalendarEventImportErrorHandlerService {
}
private async handleUnknownException(
exception: CalendarEventImportDriverException,
exception: { message: string },
calendarChannel: Pick<CalendarChannelWorkspaceEntity, 'id'>,
workspaceId: string,
): Promise<void> {
@@ -118,7 +118,10 @@ export class ConnectedAccountRefreshTokensService {
);
}
this.logger.log(error);
this.logger.log(
`Error while refreshing tokens on connected account ${connectedAccount.id.slice(0, 7)} in workspace ${workspaceId.slice(0, 7)}`,
error,
);
throw new ConnectedAccountRefreshAccessTokenException(
`Error refreshing tokens for connected account ${connectedAccount.id.slice(0, 7)} in workspace ${workspaceId.slice(0, 7)}: ${error.message} ${error?.response?.data?.error_description}`,
ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED,
@@ -1 +1 @@
export const MESSAGING_THROTTLE_MAX_ATTEMPTS = 4;
export const MESSAGING_THROTTLE_MAX_ATTEMPTS = 5;
@@ -1 +1 @@
export const MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE = 200;
export const MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE = 400;
@@ -3,6 +3,10 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import {
@@ -36,7 +40,7 @@ export class MessageImportExceptionHandlerService {
) {}
public async handleDriverException(
exception: MessageImportDriverException | Error,
exception: MessageImportDriverException | Error | TwentyORMException,
syncStep: MessageImportSyncStep,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
@@ -53,6 +57,7 @@ export class MessageImportExceptionHandlerService {
workspaceId,
);
break;
case TwentyORMExceptionCode.QUERY_READ_TIMEOUT:
case MessageImportDriverExceptionCode.TEMPORARY_ERROR:
case MessageNetworkExceptionCode.ECONNABORTED:
case MessageNetworkExceptionCode.ENOTFOUND:
@@ -102,7 +107,7 @@ export class MessageImportExceptionHandlerService {
'id' | 'throttleFailureCount'
>,
workspaceId: string,
exception: MessageImportDriverException,
exception: { message: string },
): Promise<void> {
if (
messageChannel.throttleFailureCount >= MESSAGING_THROTTLE_MAX_ATTEMPTS