Common api - Destroy and delete queries (#15177)

Done ⬇️ 
Gql : move delete and destroy logic to common
Rest : 
- rename 'delete' handler to 'destroy'
- create soft delete handlers (named 'delete') : one and many
- create destroy many handler
- update doc

--> Rest api gains NEW soft delete one/many + destroy many capabilities

closes : https://github.com/twentyhq/core-team-issues/issues/1579
This commit is contained in:
Etienne
2025-10-22 16:35:17 +02:00
committed by GitHub
parent ce87ea88f5
commit dc05f18310
50 changed files with 1800 additions and 604 deletions
@@ -1,17 +1,33 @@
import { Inject, Injectable } from '@nestjs/common';
import { type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Omit } from 'zod/v4/core/util.cjs';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import {
CommonExtendedInput,
CommonInput,
CommonQueryArgs,
CommonQueryNames,
} from 'src/engine/api/common/types/common-query-args.type';
import { CommonQueryResult } from 'src/engine/api/common/types/common-query-result.type';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
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 { 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 { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@@ -29,7 +45,10 @@ import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/wo
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Injectable()
export abstract class CommonBaseQueryRunnerService {
export abstract class CommonBaseQueryRunnerService<
Args extends CommonQueryArgs,
Output extends CommonQueryResult,
> {
@Inject()
protected readonly workspaceQueryHookService: WorkspaceQueryHookService;
@Inject()
@@ -53,82 +72,147 @@ export abstract class CommonBaseQueryRunnerService {
@Inject()
protected readonly commonResultGettersService: CommonResultGettersService;
public async prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
}: {
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}) {
if (objectMetadataItemWithFieldMaps.isSystem === true) {
await this.validateSettingsPermissionsOnObjectOrThrow(
authContext,
objectMetadataItemWithFieldMaps,
public async execute(
args: CommonInput<Args>,
queryRunnerContext: CommonBaseQueryRunnerContext,
operationName: CommonQueryNames,
): Promise<Output> {
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
queryRunnerContext;
if (!isWorkspaceAuthContext(authContext)) {
throw new CommonQueryRunnerException(
'Invalid auth context',
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
);
}
const workspace = authContext.workspace;
await this.validate(args, queryRunnerContext);
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId: workspace.id,
});
if (objectMetadataItemWithFieldMaps.isSystem === true) {
await this.validateSettingsPermissionsOnObjectOrThrow(
authContext,
queryRunnerContext,
);
}
const { roleId } = await this.getRoleIdAndObjectsPermissions(
authContext,
workspace.id,
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const rolePermissionConfig = { unionOf: [roleId] };
const repository = workspaceDataSource.getRepository(
objectMetadataItemWithFieldMaps.nameSingular,
rolePermissionConfig,
authContext,
const processedArgs = await this.processArgs(
args,
queryRunnerContext,
operationName,
commonQueryParser,
);
const extendedQueryRunnerContext =
await this.prepareExtendedQueryRunnerContext(
authContext,
queryRunnerContext,
);
const results = await this.run(processedArgs, {
...extendedQueryRunnerContext,
commonQueryParser,
});
return this.enrichResultsWithGettersAndHooks({
results,
operationName,
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
}
protected abstract run(
args: CommonExtendedInput<Args>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<Output>;
protected abstract validate(
args: CommonInput<Args>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void>;
protected abstract computeArgs(
args: CommonInput<Args>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<Args>>;
protected abstract processQueryResult(
queryResult: Output,
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
authContext: WorkspaceAuthContext,
): Promise<Output>;
private async processArgs(
args: CommonInput<Args>,
queryRunnerContext: CommonBaseQueryRunnerContext,
operationName: CommonQueryNames,
commonQueryParser: GraphqlQueryParser,
): Promise<CommonExtendedInput<Args>> {
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
args.selectedFields,
);
const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext;
const hookedArgs =
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
operationName,
args as WorkspacePreQueryHookPayload<CommonQueryNames>,
)) as CommonInput<Args>;
const computedArgs = await this.computeArgs(hookedArgs, queryRunnerContext);
return {
workspaceDataSource,
repository,
rolePermissionConfig,
...computedArgs,
selectedFieldsResult,
};
}
public async enrichResultsWithGettersAndHooks({
private async enrichResultsWithGettersAndHooks({
results,
operationName,
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
}: {
results: ObjectRecord[];
results: Output;
operationName: CommonQueryNames;
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
}): Promise<ObjectRecord[]> {
const resultWithGetters =
await this.commonResultGettersService.processQueryResult(
results,
objectMetadataItemWithFieldMaps.id,
objectMetadataMaps,
authContext.workspace.id,
);
}): Promise<Output> {
const resultWithGetters = await this.processQueryResult(
results,
objectMetadataItemWithFieldMaps.id,
objectMetadataMaps,
authContext,
);
await this.workspaceQueryHookService.executePostQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
operationName,
resultWithGetters,
resultWithGetters as QueryResultFieldValue,
);
return resultWithGetters;
return resultWithGetters as Output;
}
private async validateSettingsPermissionsOnObjectOrThrow(
authContext: WorkspaceAuthContext,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
queryRunnerContext: CommonBaseQueryRunnerContext,
) {
const { objectMetadataItemWithFieldMaps } = queryRunnerContext;
const workspace = authContext.workspace;
if (
@@ -206,4 +290,35 @@ export abstract class CommonBaseQueryRunnerService {
return { roleId, objectsPermissions: objectMetadataPermissions[roleId] };
}
private async prepareExtendedQueryRunnerContext(
authContext: WorkspaceAuthContext,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<Omit<CommonExtendedQueryRunnerContext, 'commonQueryParser'>> {
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId: authContext.workspace.id,
});
const { roleId } = await this.getRoleIdAndObjectsPermissions(
authContext,
authContext.workspace.id,
);
const rolePermissionConfig = { unionOf: [roleId] };
const repository = workspaceDataSource.getRepository(
queryRunnerContext.objectMetadataItemWithFieldMaps.nameSingular,
rolePermissionConfig,
authContext,
);
return {
...queryRunnerContext,
authContext,
workspaceDataSource,
rolePermissionConfig,
repository,
};
}
}
@@ -16,13 +16,14 @@ import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import {
CommonQueryNames,
CommonExtendedInput,
CommonInput,
CreateManyQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
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';
@@ -36,63 +37,28 @@ import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
@Injectable()
export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerService {
async run({
args,
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
}: {
args: CreateManyQueryArgs;
authContext: AuthContext;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}): Promise<ObjectRecord[]> {
if (!isWorkspaceAuthContext(authContext)) {
throw new CommonQueryRunnerException(
'Invalid auth context',
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
);
}
assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps);
// TODO : Refacto-common - Remove this validation once https://github.com/twentyhq/core-team-issues/issues/1622 done
args.data.forEach((record) => {
if (record?.id) {
assertIsValidUuid(record.id);
}
});
//TODO : Refacto-common - QueryParser should be common branded service
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
args.selectedFields,
objectMetadataMaps,
);
const { workspaceDataSource, repository, rolePermissionConfig } =
await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
const processedArgs = await this.processQueryArgs({
export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerService<
CreateManyQueryArgs,
ObjectRecord[]
> {
async run(
args: CommonExtendedInput<CreateManyQueryArgs>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<ObjectRecord[]> {
const {
repository,
authContext,
rolePermissionConfig,
objectMetadataItemWithFieldMaps,
args,
});
objectMetadataMaps,
workspaceDataSource,
} = queryRunnerContext;
const objectRecords = await this.insertOrUpsertRecords({
repository,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
args: processedArgs,
selectedFieldsResult,
args,
});
const upsertedRecords = await this.fetchUpsertedRecords({
@@ -100,63 +66,103 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
repository,
selectedFieldsResult,
selectedFieldsResult: args.selectedFieldsResult,
});
await this.processNestedRelationsIfNeeded({
args,
records: upsertedRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
rolePermissionConfig,
authContext,
workspaceDataSource,
selectedFieldsResult,
rolePermissionConfig,
});
return upsertedRecords;
}
async processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
private async processNestedRelationsIfNeeded({
args,
records,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
authContext,
workspaceDataSource,
rolePermissionConfig,
}: {
authContext: WorkspaceAuthContext;
args: CommonExtendedInput<CreateManyQueryArgs>;
records: ObjectRecord[];
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
args: CreateManyQueryArgs;
}): Promise<CreateManyQueryArgs> {
const hookedArgs =
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
CommonQueryNames.CREATE_MANY,
args,
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
)) as CreateManyQueryArgs;
objectMetadataMaps: ObjectMetadataMaps;
authContext: AuthContext;
workspaceDataSource: WorkspaceDataSource;
rolePermissionConfig?: RolePermissionConfig;
}): Promise<void> {
if (!args.selectedFieldsResult.relations) {
return;
}
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: records,
relations: args.selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource,
rolePermissionConfig,
selectedFields: args.selectedFieldsResult.select,
});
}
async computeArgs(
args: CommonInput<CreateManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<CreateManyQueryArgs>> {
const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext;
return {
...hookedArgs,
...args,
data: await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
partialRecordInputs: hookedArgs.data,
partialRecordInputs: args.data,
authContext,
objectMetadataItemWithFieldMaps,
}),
};
}
async validate(
args: CommonInput<CreateManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void> {
const { objectMetadataItemWithFieldMaps } = queryRunnerContext;
assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps);
args.data.forEach((record) => {
if (record?.id) {
assertIsValidUuid(record.id);
}
});
}
private async insertOrUpsertRecords({
repository,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
args,
selectedFieldsResult,
}: {
repository: WorkspaceRepository<ObjectLiteral>;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
args: CreateManyQueryArgs;
selectedFieldsResult: CommonSelectedFieldsResult;
args: CommonExtendedInput<CreateManyQueryArgs>;
}): Promise<InsertResult> {
const { selectedFieldsResult } = args;
if (!args.upsert) {
const selectedColumns = buildColumnsToReturn({
select: selectedFieldsResult.select,
@@ -380,42 +386,18 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
return upsertedRecords as ObjectRecord[];
}
private async processNestedRelationsIfNeeded({
records,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
rolePermissionConfig,
authContext,
workspaceDataSource,
selectedFieldsResult,
}: {
records: ObjectRecord[];
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
authContext: AuthContext;
workspaceDataSource: WorkspaceDataSource;
rolePermissionConfig?: RolePermissionConfig;
selectedFieldsResult: CommonSelectedFieldsResult;
}): Promise<void> {
if (!selectedFieldsResult.relations) {
return;
}
await this.processNestedRelationsHelper.processNestedRelations({
async processQueryResult(
queryResult: ObjectRecord[],
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
authContext: WorkspaceAuthContext,
): Promise<ObjectRecord[]> {
return await this.commonResultGettersService.processRecordArray(
queryResult,
objectMetadataItemId,
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: records,
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
relations: selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource,
rolePermissionConfig,
selectedFields: selectedFieldsResult.select,
});
authContext.workspace.id,
);
}
private getRecordWithoutCreatedBy(
@@ -2,42 +2,74 @@ import { Injectable } from '@nestjs/common';
import { type ObjectRecord } from 'twenty-shared/types';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
import { CreateOneQueryArgs } from 'src/engine/api/common/types/common-query-args.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import {
CommonExtendedInput,
CommonInput,
CreateManyQueryArgs,
CreateOneQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
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 { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@Injectable()
export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerService {
export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerService<
CreateOneQueryArgs,
ObjectRecord
> {
constructor(
private readonly commonCreateManyQueryRunnerService: CommonCreateManyQueryRunnerService,
) {
super();
}
async run({
args,
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
}: {
args: CreateOneQueryArgs;
authContext: AuthContext;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}): Promise<ObjectRecord> {
const result = await this.commonCreateManyQueryRunnerService.run({
args: {
async run(
args: CommonExtendedInput<CreateManyQueryArgs>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<ObjectRecord> {
const result = await this.commonCreateManyQueryRunnerService.run(
{
...args,
data: [args.data],
},
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
});
queryRunnerContext,
);
return result[0];
}
async computeArgs(
args: CommonInput<CreateOneQueryArgs>,
_queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<CreateOneQueryArgs>> {
return args;
}
async processQueryResult(
queryResult: ObjectRecord,
_objectMetadataItemId: string,
_objectMetadataMaps: ObjectMetadataMaps,
_authContext: WorkspaceAuthContext,
): Promise<ObjectRecord> {
return queryResult;
}
async validate(
args: CommonInput<CreateOneQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void> {
const { objectMetadataItemWithFieldMaps } = queryRunnerContext;
assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps);
if (args.data?.id) {
assertIsValidUuid(args.data.id);
}
}
}
@@ -0,0 +1,142 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { ObjectRecord } from 'twenty-shared/types';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import {
CommonExtendedInput,
CommonInput,
DeleteManyQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
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 { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerService<
DeleteManyQueryArgs,
ObjectRecord[]
> {
async run(
args: CommonExtendedInput<DeleteManyQueryArgs>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<ObjectRecord[]> {
const {
repository,
authContext,
rolePermissionConfig,
workspaceDataSource,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
commonQueryParser,
} = queryRunnerContext;
const queryBuilder = repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
commonQueryParser.applyFilterToBuilder(
queryBuilder,
tableName,
args.filter,
);
const columnsToReturn = buildColumnsToReturn({
select: args.selectedFieldsResult.select,
relations: args.selectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const deletedObjectRecords = await queryBuilder
.softDelete()
.returning(columnsToReturn)
.execute();
const deletedRecords = deletedObjectRecords.generatedMaps as ObjectRecord[];
if (isDefined(args.selectedFieldsResult.relations)) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: deletedRecords,
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
relations: args.selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource,
rolePermissionConfig,
selectedFields: args.selectedFieldsResult.select,
});
}
return deletedRecords;
}
async computeArgs(
args: CommonInput<DeleteManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<DeleteManyQueryArgs>> {
const { objectMetadataItemWithFieldMaps } = queryRunnerContext;
return {
...args,
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
args.filter,
objectMetadataItemWithFieldMaps,
),
};
}
async processQueryResult(
queryResult: ObjectRecord[],
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
authContext: WorkspaceAuthContext,
): Promise<ObjectRecord[]> {
return this.commonResultGettersService.processRecordArray(
queryResult,
objectMetadataItemId,
objectMetadataMaps,
authContext.workspace.id,
);
}
async validate(
args: DeleteManyQueryArgs,
queryRunnerContext: CommonBaseQueryRunnerContext,
) {
const { objectMetadataItemWithFieldMaps } = queryRunnerContext;
assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps);
if (!args.filter) {
throw new CommonQueryRunnerException(
'Filter is required',
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id));
}
}
@@ -0,0 +1,83 @@
import { Injectable } from '@nestjs/common';
import { type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import {
CommonExtendedInput,
CommonInput,
DeleteOneQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
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 { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@Injectable()
export class CommonDeleteOneQueryRunnerService extends CommonBaseQueryRunnerService<
DeleteOneQueryArgs,
ObjectRecord
> {
constructor(
private readonly commonDeleteManyQueryRunnerService: CommonDeleteManyQueryRunnerService,
) {
super();
}
async run(
args: CommonExtendedInput<DeleteOneQueryArgs>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<ObjectRecord> {
const result = await this.commonDeleteManyQueryRunnerService.run(
{
...args,
filter: { id: { eq: args.id } },
},
queryRunnerContext,
);
if (!isDefined(result) || result.length === 0) {
throw new CommonQueryRunnerException(
'Record not found',
CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
return result[0];
}
async computeArgs(
args: CommonInput<DeleteOneQueryArgs>,
_queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<DeleteOneQueryArgs>> {
return args;
}
async processQueryResult(
queryResult: ObjectRecord,
_objectMetadataItemId: string,
_objectMetadataMaps: ObjectMetadataMaps,
_authContext: WorkspaceAuthContext,
): Promise<ObjectRecord> {
return queryResult;
}
async validate(
args: CommonInput<DeleteOneQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void> {
const { objectMetadataItemWithFieldMaps } = queryRunnerContext;
assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps);
assertIsValidUuid(args.id);
}
}
@@ -0,0 +1,129 @@
import { Injectable } from '@nestjs/common';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import {
CommonExtendedInput,
CommonInput,
DestroyManyQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@Injectable()
export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerService<
DestroyManyQueryArgs,
ObjectRecord[]
> {
async run(
args: CommonExtendedInput<DestroyManyQueryArgs>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<ObjectRecord[]> {
const {
repository,
authContext,
rolePermissionConfig,
workspaceDataSource,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
commonQueryParser,
} = queryRunnerContext;
const queryBuilder = repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
);
commonQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
args.filter,
);
const columnsToReturn = buildColumnsToReturn({
select: args.selectedFieldsResult.select,
relations: args.selectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const deletedObjectRecords = await queryBuilder
.delete()
.returning(columnsToReturn)
.execute();
const deletedRecords = deletedObjectRecords.generatedMaps as ObjectRecord[];
if (isDefined(args.selectedFieldsResult.relations)) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: deletedRecords,
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
relations: args.selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource,
rolePermissionConfig,
selectedFields: args.selectedFieldsResult.select,
});
}
return deletedRecords;
}
async computeArgs(
args: CommonInput<DestroyManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<DestroyManyQueryArgs>> {
const { objectMetadataItemWithFieldMaps } = queryRunnerContext;
return {
...args,
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
args.filter,
objectMetadataItemWithFieldMaps,
),
};
}
async processQueryResult(
queryResult: ObjectRecord[],
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
authContext: WorkspaceAuthContext,
): Promise<ObjectRecord[]> {
return this.commonResultGettersService.processRecordArray(
queryResult,
objectMetadataItemId,
objectMetadataMaps,
authContext.workspace.id,
);
}
async validate(
args: CommonInput<DestroyManyQueryArgs>,
_queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void> {
if (!isDefined(args.filter)) {
throw new CommonQueryRunnerException(
'Filter is required',
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
}
}
@@ -0,0 +1,83 @@
import { Injectable } from '@nestjs/common';
import { type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import {
CommonExtendedInput,
CommonInput,
DestroyOneQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@Injectable()
export class CommonDestroyOneQueryRunnerService extends CommonBaseQueryRunnerService<
DestroyOneQueryArgs,
ObjectRecord
> {
constructor(
private readonly commonDestroyManyQueryRunnerService: CommonDestroyManyQueryRunnerService,
) {
super();
}
async run(
args: CommonExtendedInput<DestroyOneQueryArgs>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<ObjectRecord> {
const result = await this.commonDestroyManyQueryRunnerService.run(
{
...args,
filter: { id: { eq: args.id } },
},
queryRunnerContext,
);
if (!isDefined(result) || result.length === 0) {
throw new CommonQueryRunnerException(
'Record not found',
CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
return result[0];
}
async computeArgs(
args: CommonInput<DestroyOneQueryArgs>,
_queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<DestroyOneQueryArgs>> {
return args;
}
async processQueryResult(
queryResult: ObjectRecord,
_objectMetadataItemId: string,
_objectMetadataMaps: ObjectMetadataMaps,
_authContext: WorkspaceAuthContext,
): Promise<ObjectRecord> {
return queryResult;
}
async validate(
args: CommonInput<DestroyOneQueryArgs>,
_queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void> {
if (!isDefined(args.id)) {
throw new CommonQueryRunnerException(
'Missing id',
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
}
}
@@ -16,73 +16,39 @@ import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { CommonPageInfo } from 'src/engine/api/common/types/common-page-info.type';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import { CommonFindManyOutput } from 'src/engine/api/common/types/common-find-many-output.type';
import {
CommonQueryNames,
CommonExtendedInput,
CommonInput,
FindManyQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { getCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
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';
@Injectable()
export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService {
async run({
args,
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
}: {
args: FindManyQueryArgs;
authContext: AuthContext;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}): Promise<{
records: ObjectRecord[];
aggregatedValues: Record<string, number>;
totalCount: number;
pageInfo: CommonPageInfo;
selectedFieldsResult: CommonSelectedFieldsResult;
}> {
this.validate(args);
if (!isWorkspaceAuthContext(authContext)) {
throw new CommonQueryRunnerException(
'Invalid auth context',
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
);
}
const { workspaceDataSource, repository, rolePermissionConfig } =
await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
args.selectedFields,
objectMetadataMaps,
);
const processedArgs = await this.processQueryArgs({
export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService<
FindManyQueryArgs,
CommonFindManyOutput
> {
async run(
args: CommonExtendedInput<FindManyQueryArgs>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<CommonFindManyOutput> {
const {
repository,
authContext,
rolePermissionConfig,
objectMetadataItemWithFieldMaps,
args,
});
objectMetadataMaps,
workspaceDataSource,
commonQueryParser,
} = queryRunnerContext;
const queryBuilder = repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
@@ -90,7 +56,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
const aggregateQueryBuilder = queryBuilder.clone();
let appliedFilters = processedArgs.filter ?? ({} as ObjectRecordFilter);
let appliedFilters = args.filter ?? ({} as ObjectRecordFilter);
commonQueryParser.applyFilterToBuilder(
aggregateQueryBuilder,
@@ -104,13 +70,13 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
);
const orderByWithIdCondition = [
...(processedArgs.orderBy ?? []),
...(args.orderBy ?? []),
{ id: OrderByDirection.AscNullsFirst },
] as ObjectRecordOrderBy;
const isForwardPagination = !isDefined(processedArgs.before);
const isForwardPagination = !isDefined(args.before);
const cursor = getCursor(processedArgs);
const cursor = getCursor(args);
if (cursor) {
const cursorArgFilter = computeCursorArgFilter(
@@ -120,9 +86,9 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
isForwardPagination,
);
appliedFilters = (processedArgs.filter
appliedFilters = (args.filter
? {
and: [processedArgs.filter, { or: cursorArgFilter }],
and: [args.filter, { or: cursorArgFilter }],
}
: { or: cursorArgFilter }) as unknown as ObjectRecordFilter;
}
@@ -143,17 +109,16 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
selectedAggregatedFields: selectedFieldsResult.aggregate,
selectedAggregatedFields: args.selectedFieldsResult.aggregate,
queryBuilder: aggregateQueryBuilder,
objectMetadataNameSingular: objectMetadataItemWithFieldMaps.nameSingular,
});
const limit =
processedArgs.first ?? processedArgs.last ?? QUERY_MAX_RECORDS;
const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS;
const columnsToSelect = buildColumnsToSelect({
select: selectedFieldsResult.select,
relations: selectedFieldsResult.relations,
select: args.selectedFieldsResult.select,
relations: args.selectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
@@ -183,44 +148,74 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
const parentObjectRecordsAggregatedValues =
await aggregateQueryBuilder.getRawOne();
if (selectedFieldsResult.relations) {
if (isDefined(args.selectedFieldsResult.relations)) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: objectRecords,
parentObjectRecordsAggregatedValues,
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
relations: selectedFieldsResult.relations as Record<
relations: args.selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
aggregate: selectedFieldsResult.aggregate,
aggregate: args.selectedFieldsResult.aggregate,
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource,
rolePermissionConfig,
selectedFields: selectedFieldsResult.select,
selectedFields: args.selectedFieldsResult.select,
});
}
const enrichedRecords = await this.enrichResultsWithGettersAndHooks({
results: objectRecords,
operationName: CommonQueryNames.FIND_MANY,
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
return {
records: enrichedRecords,
records: objectRecords,
aggregatedValues: parentObjectRecordsAggregatedValues,
totalCount: parentObjectRecordsAggregatedValues?.totalCount,
pageInfo,
selectedFieldsResult,
selectedFieldsResult: args.selectedFieldsResult,
};
}
validate(args: FindManyQueryArgs) {
async computeArgs(
args: CommonInput<FindManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<FindManyQueryArgs>> {
const { objectMetadataItemWithFieldMaps } = queryRunnerContext;
return {
...args,
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
args.filter,
objectMetadataItemWithFieldMaps,
),
};
}
async processQueryResult(
queryResult: CommonFindManyOutput,
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
authContext: WorkspaceAuthContext,
): Promise<CommonFindManyOutput> {
const processedRecords =
await this.commonResultGettersService.processRecordArray(
queryResult.records,
objectMetadataItemId,
objectMetadataMaps,
authContext.workspace.id,
);
return {
...queryResult,
records: processedRecords,
};
}
async validate(
args: CommonInput<FindManyQueryArgs>,
_queryRunnerContext: CommonBaseQueryRunnerContext,
) {
if (args.first && args.last) {
throw new CommonQueryRunnerException(
'Cannot provide both first and last',
@@ -258,31 +253,4 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
);
}
}
async processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
args,
}: {
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
args: FindManyQueryArgs;
}): Promise<FindManyQueryArgs> {
const hookedArgs =
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
CommonQueryNames.FIND_MANY,
args,
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
)) as FindManyQueryArgs;
return {
...hookedArgs,
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
hookedArgs.filter,
objectMetadataItemWithFieldMaps,
),
};
}
}
@@ -13,70 +13,34 @@ import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import {
CommonQueryNames,
CommonExtendedInput,
CommonInput,
FindOneQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
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';
@Injectable()
export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService {
async run({
args,
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
}: {
args: FindOneQueryArgs;
authContext: AuthContext;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}): Promise<ObjectRecord> {
if (!isWorkspaceAuthContext(authContext)) {
throw new CommonQueryRunnerException(
'Invalid auth context',
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
);
}
const { workspaceDataSource, repository, rolePermissionConfig } =
await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
//TODO : Refacto-common - QueryParser should be common branded service
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
args.selectedFields,
objectMetadataMaps,
);
const processedArgs = await this.processQueryArgs({
export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService<
FindOneQueryArgs,
ObjectRecord
> {
async run(
args: CommonExtendedInput<FindOneQueryArgs>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<ObjectRecord> {
const {
repository,
authContext,
rolePermissionConfig,
workspaceDataSource,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
args,
});
if (
!processedArgs.filter ||
Object.keys(processedArgs.filter).length === 0
) {
throw new CommonQueryRunnerException(
'Missing filter argument',
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
commonQueryParser,
} = queryRunnerContext;
const queryBuilder = repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
@@ -85,17 +49,17 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
commonQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
processedArgs.filter ?? ({} as ObjectRecordFilter),
args.filter ?? ({} as ObjectRecordFilter),
);
commonQueryParser.applyDeletedAtToBuilder(
queryBuilder,
processedArgs.filter ?? ({} as ObjectRecordFilter),
args.filter ?? ({} as ObjectRecordFilter),
);
const columnsToSelect = buildColumnsToSelect({
select: selectedFieldsResult.select,
relations: selectedFieldsResult.relations,
select: args.selectedFieldsResult.select,
relations: args.selectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
@@ -115,13 +79,13 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
const objectRecords = [objectRecord] as ObjectRecord[];
if (isDefined(selectedFieldsResult.relations)) {
if (isDefined(args.selectedFieldsResult.relations)) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: objectRecords,
//TODO : Refacto-common - To fix when switching processNestedRelationsHelper to Common
relations: selectedFieldsResult.relations as Record<
relations: args.selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
@@ -129,45 +93,51 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
authContext,
workspaceDataSource,
rolePermissionConfig,
selectedFields: selectedFieldsResult.select,
selectedFields: args.selectedFieldsResult.select,
});
}
const enrichedResults = await this.enrichResultsWithGettersAndHooks({
results: objectRecords,
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
operationName: CommonQueryNames.FIND_ONE,
});
return enrichedResults[0];
return objectRecords[0];
}
async processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
args,
}: {
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
args: FindOneQueryArgs;
}): Promise<FindOneQueryArgs> {
const hookedArgs =
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
CommonQueryNames.FIND_ONE,
args,
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
)) as FindOneQueryArgs;
async computeArgs(
args: CommonInput<FindOneQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<FindOneQueryArgs>> {
const { objectMetadataItemWithFieldMaps } = queryRunnerContext;
return {
...hookedArgs,
...args,
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
hookedArgs.filter,
args.filter,
objectMetadataItemWithFieldMaps,
),
};
}
async processQueryResult(
queryResult: ObjectRecord,
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
authContext: WorkspaceAuthContext,
): Promise<ObjectRecord> {
return this.commonResultGettersService.processRecord(
queryResult,
objectMetadataItemId,
objectMetadataMaps,
authContext.workspace.id,
);
}
async validate(
args: CommonInput<FindOneQueryArgs>,
_queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void> {
if (!args.filter || Object.keys(args.filter).length === 0) {
throw new CommonQueryRunnerException(
'Missing filter argument',
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
}
}
@@ -11,6 +11,7 @@ import {
computeRecordGqlOperationFilter,
convertViewFilterValueToString,
getFilterTypeFromFieldType,
isDefined,
turnAnyFieldFilterIntoRecordGqlFilter,
} from 'twenty-shared/utils';
@@ -18,24 +19,20 @@ import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
import {
CommonQueryNames,
CommonExtendedInput,
CommonInput,
GroupByQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util';
import { getGroupByExpression } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util';
import { isGroupByDateField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field.util';
import { parseGroupByArgs } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util';
import { removeQuotes } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/remove-quote.util';
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
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 { ViewFilterGroupService } from 'src/engine/metadata-modules/view-filter-group/services/view-filter-group.service';
@@ -45,7 +42,10 @@ import { ViewService } from 'src/engine/metadata-modules/view/services/view.serv
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
@Injectable()
export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerService {
export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerService<
GroupByQueryArgs,
CommonGroupByOutputItem[]
> {
constructor(
private readonly viewFilterService: ViewFilterService,
private readonly viewFilterGroupService: ViewFilterGroupService,
@@ -54,46 +54,16 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
super();
}
async run({
args,
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
}: {
args: GroupByQueryArgs;
authContext: AuthContext;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}): Promise<CommonGroupByOutputItem[]> {
if (!isWorkspaceAuthContext(authContext)) {
throw new CommonQueryRunnerException(
'Invalid auth context',
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
);
}
const { repository } = await this.prepareQueryRunnerContext({
async run(
args: CommonExtendedInput<GroupByQueryArgs>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<CommonGroupByOutputItem[]> {
const {
repository,
commonQueryParser,
objectMetadataItemWithFieldMaps,
authContext,
objectMetadataItemWithFieldMaps,
});
//TODO : Refacto-common - QueryParser should be common branded service
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
args.selectedFields,
objectMetadataMaps,
);
const processedArgs = await this.processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
args,
});
} = queryRunnerContext;
const objectMetadataNameSingular =
objectMetadataItemWithFieldMaps.nameSingular;
@@ -104,9 +74,9 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
let appliedFilters = args.filter ?? ({} as ObjectRecordFilter);
if (args.viewId) {
if (isDefined(args.viewId)) {
appliedFilters = await this.addFiltersFromView({
args: processedArgs,
args,
authContext,
objectMetadataItemWithFieldMaps,
appliedFilters,
@@ -122,13 +92,13 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
selectedAggregatedFields: selectedFieldsResult.aggregate,
selectedAggregatedFields: args.selectedFieldsResult.aggregate,
queryBuilder,
objectMetadataNameSingular,
});
const groupByFields = parseGroupByArgs(
processedArgs,
args,
objectMetadataItemWithFieldMaps,
);
@@ -170,7 +140,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
commonQueryParser.applyGroupByOrderToBuilder(
queryBuilder,
processedArgs.orderBy ?? [],
args.orderBy ?? [],
groupByFields,
);
@@ -179,10 +149,19 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
return formatResultWithGroupByDimensionValues(
result,
groupByDefinitions,
Object.keys(selectedFieldsResult.aggregate),
Object.keys(args.selectedFieldsResult.aggregate),
);
}
async processQueryResult(
queryResult: CommonGroupByOutputItem[],
_objectMetadataItemId: string,
_objectMetadataMaps: ObjectMetadataMaps,
_authContext: WorkspaceAuthContext,
): Promise<CommonGroupByOutputItem[]> {
return queryResult;
}
private async addFiltersFromView({
args,
objectMetadataItemWithFieldMaps,
@@ -271,28 +250,21 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
return appliedFilters;
}
async processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
args,
}: {
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
args: GroupByQueryArgs;
}): Promise<GroupByQueryArgs> {
const hookedArgs =
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
CommonQueryNames.GROUP_BY,
args,
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
)) as GroupByQueryArgs;
async validate(
_args: CommonInput<GroupByQueryArgs>,
_queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void> {}
async computeArgs(
args: CommonInput<GroupByQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<GroupByQueryArgs>> {
const { objectMetadataItemWithFieldMaps } = queryRunnerContext;
return {
...hookedArgs,
...args,
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
hookedArgs.filter,
args.filter,
objectMetadataItemWithFieldMaps,
),
};
@@ -1,5 +1,9 @@
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service';
import { CommonDeleteOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-one-query-runner.service';
import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service';
import { CommonDestroyOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service';
import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service';
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service';
@@ -10,4 +14,8 @@ export const CommonQueryRunners = [
CommonCreateOneQueryRunnerService,
CommonCreateManyQueryRunnerService,
CommonGroupByQueryRunnerService,
CommonDestroyOneQueryRunnerService,
CommonDestroyManyQueryRunnerService,
CommonDeleteOneQueryRunnerService,
CommonDeleteManyQueryRunnerService,
];
@@ -43,7 +43,7 @@ export class CommonResultGettersService {
]);
}
private async processRecordArray(
public async processRecordArray(
recordArray: ObjectRecord[],
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
@@ -62,7 +62,7 @@ export class CommonResultGettersService {
);
}
private async processRecord(
public async processRecord(
record: ObjectRecord,
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
@@ -133,20 +133,6 @@ export class CommonResultGettersService {
return processedRecord;
}
async processQueryResult(
queryResultField: ObjectRecord[],
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
): Promise<ObjectRecord[]> {
return await this.processRecordArray(
queryResultField,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
);
}
private getHandler(objectType: string): QueryResultGetterHandlerInterface {
return (
this.handlers.get(objectType) || {
@@ -0,0 +1,9 @@
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export type CommonBaseQueryRunnerContext = {
authContext: AuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
};
@@ -0,0 +1,20 @@
import { type ObjectLiteral } from 'typeorm';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { type GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
export type CommonExtendedQueryRunnerContext = Omit<
CommonBaseQueryRunnerContext,
'authContext'
> & {
authContext: WorkspaceAuthContext;
rolePermissionConfig: RolePermissionConfig;
repository: WorkspaceRepository<ObjectLiteral>;
commonQueryParser: GraphqlQueryParser;
workspaceDataSource: WorkspaceDataSource;
};
@@ -0,0 +1,12 @@
import { type ObjectRecord } from 'twenty-shared/types';
import { type CommonPageInfo } from 'src/engine/api/common/types/common-page-info.type';
import { type CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
export type CommonFindManyOutput = {
records: ObjectRecord[];
aggregatedValues: Record<string, number>;
totalCount: number;
pageInfo: CommonPageInfo;
selectedFieldsResult: CommonSelectedFieldsResult;
};
@@ -9,22 +9,35 @@ import {
type ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type';
import {
type CommonSelectedFields,
type CommonSelectedFieldsResult,
} from 'src/engine/api/common/types/common-selected-fields-result.type';
export enum CommonQueryNames {
FIND_ONE = 'findOne',
FIND_MANY = 'findMany',
CREATE_ONE = 'createOne',
CREATE_MANY = 'createMany',
GROUP_BY = 'groupBy',
DESTROY_MANY = 'destroyMany',
DESTROY_ONE = 'destroyOne',
DELETE_MANY = 'deleteMany',
DELETE_ONE = 'deleteOne',
}
export interface FindOneQueryArgs {
interface BaseQueryArgs {
selectedFields: CommonSelectedFields;
}
interface ExtendedBaseQueryArgs {
selectedFieldsResult: CommonSelectedFieldsResult;
}
export interface FindOneQueryArgs extends BaseQueryArgs {
filter?: ObjectRecordFilter;
}
export interface FindManyQueryArgs {
selectedFields: CommonSelectedFields;
filter?: ObjectRecordFilter;
orderBy?: ObjectRecordOrderBy;
first?: number;
@@ -34,25 +47,48 @@ export interface FindManyQueryArgs {
}
export interface CreateManyQueryArgs {
selectedFields: CommonSelectedFields;
data: Partial<ObjectRecord>[];
upsert?: boolean;
}
export interface CreateOneQueryArgs {
selectedFields: CommonSelectedFields;
data: Partial<ObjectRecord>;
upsert?: boolean;
}
export interface GroupByQueryArgs {
selectedFields: CommonSelectedFields;
filter?: ObjectRecordFilter;
orderBy?: OrderByWithGroupBy;
groupBy: ObjectRecordGroupBy;
viewId?: string;
}
export interface DestroyOneQueryArgs {
id: string;
}
export interface DestroyManyQueryArgs {
filter: ObjectRecordFilter;
}
export interface DeleteOneQueryArgs {
id: string;
}
export interface DeleteManyQueryArgs {
filter: ObjectRecordFilter;
}
export type CommonQueryArgs =
| FindOneQueryArgs
| FindManyQueryArgs
| GroupByQueryArgs;
| CreateManyQueryArgs
| CreateOneQueryArgs
| GroupByQueryArgs
| DestroyOneQueryArgs
| DestroyManyQueryArgs
| DeleteOneQueryArgs
| DeleteManyQueryArgs;
export type CommonInput<T extends CommonQueryArgs> = T & BaseQueryArgs;
export type CommonExtendedInput<T extends CommonQueryArgs> = T &
ExtendedBaseQueryArgs;
@@ -0,0 +1,10 @@
import { type ObjectRecord } from 'twenty-shared/types';
import { type CommonFindManyOutput } from 'src/engine/api/common/types/common-find-many-output.type';
import { type CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
export type CommonQueryResult =
| ObjectRecord[]
| ObjectRecord
| CommonGroupByOutputItem[]
| CommonFindManyOutput;