Files
twenty/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts
T
Etienne d1befa7e35 Query complexity validation (#16274)
Validations : 
- relations count (in common api)
- oneToMany relation nested count (in common)
- requested fields count (in gql)
- root resolver count (in gql)
- root resolver duplicates (in gql)
- specific complexity for metadata / nesting count (in gql)
2025-12-04 16:33:08 +01:00

149 lines
5.1 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { QUERY_MAX_RECORDS_FROM_RELATION } 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,
CommonQueryNames,
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 { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
@Injectable()
export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerService<
DestroyManyQueryArgs,
ObjectRecord[]
> {
protected readonly operationName = CommonQueryNames.DESTROY_MANY;
async run(
args: CommonExtendedInput<DestroyManyQueryArgs>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<ObjectRecord[]> {
const {
repository,
authContext,
rolePermissionConfig,
workspaceDataSource,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatObjectMetadata,
commonQueryParser,
} = queryRunnerContext;
const queryBuilder = repository.createQueryBuilder(
flatObjectMetadata.nameSingular,
);
commonQueryParser.applyFilterToBuilder(
queryBuilder,
flatObjectMetadata.nameSingular,
args.filter,
);
const columnsToReturn = buildColumnsToReturn({
select: args.selectedFieldsResult.select,
relations: args.selectedFieldsResult.relations,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
});
const destroyedObjectRecords = await queryBuilder
.delete()
.returning(columnsToReturn)
.execute();
const destroyedRecords =
destroyedObjectRecords.generatedMaps as ObjectRecord[];
if (isDefined(args.selectedFieldsResult.relations)) {
await this.processNestedRelationsHelper.processNestedRelations({
flatObjectMetadataMaps,
flatFieldMetadataMaps,
parentObjectMetadataItem: flatObjectMetadata,
parentObjectRecords: destroyedRecords,
relations: args.selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS_FROM_RELATION,
authContext,
workspaceDataSource,
rolePermissionConfig,
selectedFields: args.selectedFieldsResult.select,
});
}
return destroyedRecords;
}
async computeArgs(
args: CommonInput<DestroyManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<DestroyManyQueryArgs>> {
const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext;
return {
...args,
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
args.filter,
flatObjectMetadata,
flatFieldMetadataMaps,
),
};
}
async processQueryResult(
queryResult: ObjectRecord[],
flatObjectMetadata: FlatObjectMetadata,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
authContext: WorkspaceAuthContext,
): Promise<ObjectRecord[]> {
return this.commonResultGettersService.processRecordArray(
queryResult,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
authContext.workspace.id,
);
}
async validate(
args: CommonInput<DestroyManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void> {
const { flatObjectMetadata } = queryRunnerContext;
assertMutationNotOnRemoteObject(flatObjectMetadata);
if (!isDefined(args.filter)) {
throw new CommonQueryRunnerException(
'Filter is required',
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id));
}
}