fix: impact too many records (#3993)

* fix: impact too many records

* fix: change env name

* fix: remove env name from error
This commit is contained in:
Jérémy M
2024-02-16 11:17:37 +01:00
committed by GitHub
parent c2c14d79a9
commit 44ac16c82e
12 changed files with 107 additions and 39 deletions

View File

@ -55,3 +55,4 @@ SIGN_IN_PREFILLED=true
# PASSWORD_RESET_TOKEN_EXPIRES_IN=5m # PASSWORD_RESET_TOKEN_EXPIRES_IN=5m
# API_RATE_LIMITING_TTL= # API_RATE_LIMITING_TTL=
# API_RATE_LIMITING_LIMIT= # API_RATE_LIMITING_LIMIT=
# MUTATION_MAXIMUM_RECORD_AFFECTED=100

View File

@ -21,3 +21,4 @@ REFRESH_TOKEN_SECRET=secret_refresh_token
# MESSAGING_PROVIDER_GMAIL_ENABLED=false # MESSAGING_PROVIDER_GMAIL_ENABLED=false
# STORAGE_TYPE=local # STORAGE_TYPE=local
# STORAGE_LOCAL_PATH=.local-storage # STORAGE_LOCAL_PATH=.local-storage
# MUTATION_MAXIMUM_RECORD_AFFECTED=100

View File

@ -299,4 +299,10 @@ export class EnvironmentService {
getApiRateLimitingLimit(): number { getApiRateLimitingLimit(): number {
return this.configService.get<number>('API_RATE_LIMITING_LIMIT') ?? 500; return this.configService.get<number>('API_RATE_LIMITING_LIMIT') ?? 500;
} }
getMutationMaximumRecordAffected(): number {
return (
this.configService.get<number>('MUTATION_MAXIMUM_RECORD_AFFECTED') ?? 100
);
}
} }

View File

@ -199,6 +199,11 @@ export class EnvironmentVariables {
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
IS_SIGN_UP_DISABLED?: boolean; IS_SIGN_UP_DISABLED?: boolean;
@CastToPositiveNumber()
@IsOptional()
@IsNumber()
MUTATION_MAXIMUM_RECORD_AFFECTED: number;
} }
export const validate = (config: Record<string, unknown>) => { export const validate = (config: Record<string, unknown>) => {

View File

@ -8,13 +8,18 @@ import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-tar
import { FieldsStringFactory } from './fields-string.factory'; import { FieldsStringFactory } from './fields-string.factory';
export interface DeleteManyQueryFactoryOptions
extends WorkspaceQueryBuilderOptions {
atMost?: number;
}
@Injectable() @Injectable()
export class DeleteManyQueryFactory { export class DeleteManyQueryFactory {
constructor(private readonly fieldsStringFactory: FieldsStringFactory) {} constructor(private readonly fieldsStringFactory: FieldsStringFactory) {}
async create( async create(
args: DeleteManyResolverArgs, args: DeleteManyResolverArgs,
options: WorkspaceQueryBuilderOptions, options: DeleteManyQueryFactoryOptions,
) { ) {
const fieldsString = await this.fieldsStringFactory.create( const fieldsString = await this.fieldsStringFactory.create(
options.info, options.info,
@ -28,7 +33,7 @@ export class DeleteManyQueryFactory {
options.objectMetadataItem, options.objectMetadataItem,
)}Collection(filter: ${stringifyWithoutKeyQuote( )}Collection(filter: ${stringifyWithoutKeyQuote(
args.filter, args.filter,
)}, atMost: 30) { )}, atMost: ${options.atMost ?? 1}) {
affectedCount affectedCount
records { records {
${fieldsString} ${fieldsString}

View File

@ -3,7 +3,7 @@ import { ArgsStringFactory } from './args-string.factory';
import { RelationFieldAliasFactory } from './relation-field-alias.factory'; import { RelationFieldAliasFactory } from './relation-field-alias.factory';
import { CreateManyQueryFactory } from './create-many-query.factory'; import { CreateManyQueryFactory } from './create-many-query.factory';
import { DeleteOneQueryFactory } from './delete-one-query.factory'; import { DeleteOneQueryFactory } from './delete-one-query.factory';
import { FieldAliasFacotry } from './field-alias.factory'; import { FieldAliasFactory } from './field-alias.factory';
import { FieldsStringFactory } from './fields-string.factory'; import { FieldsStringFactory } from './fields-string.factory';
import { FindManyQueryFactory } from './find-many-query.factory'; import { FindManyQueryFactory } from './find-many-query.factory';
import { FindOneQueryFactory } from './find-one-query.factory'; import { FindOneQueryFactory } from './find-one-query.factory';
@ -17,7 +17,7 @@ export const workspaceQueryBuilderFactories = [
RelationFieldAliasFactory, RelationFieldAliasFactory,
CreateManyQueryFactory, CreateManyQueryFactory,
DeleteOneQueryFactory, DeleteOneQueryFactory,
FieldAliasFacotry, FieldAliasFactory,
FieldsStringFactory, FieldsStringFactory,
FindManyQueryFactory, FindManyQueryFactory,
FindOneQueryFactory, FindOneQueryFactory,

View File

@ -3,8 +3,8 @@ import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
@Injectable() @Injectable()
export class FieldAliasFacotry { export class FieldAliasFactory {
private readonly logger = new Logger(FieldAliasFacotry.name); private readonly logger = new Logger(FieldAliasFactory.name);
create(fieldKey: string, fieldMetadata: FieldMetadataInterface) { create(fieldKey: string, fieldMetadata: FieldMetadataInterface) {
const entries = Object.entries(fieldMetadata.targetColumnMap); const entries = Object.entries(fieldMetadata.targetColumnMap);

View File

@ -9,7 +9,7 @@ import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/
import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util'; import { isRelationFieldMetadataType } from 'src/workspace/utils/is-relation-field-metadata-type.util';
import { FieldAliasFacotry } from './field-alias.factory'; import { FieldAliasFactory } from './field-alias.factory';
import { RelationFieldAliasFactory } from './relation-field-alias.factory'; import { RelationFieldAliasFactory } from './relation-field-alias.factory';
@Injectable() @Injectable()
@ -17,7 +17,7 @@ export class FieldsStringFactory {
private readonly logger = new Logger(FieldsStringFactory.name); private readonly logger = new Logger(FieldsStringFactory.name);
constructor( constructor(
private readonly fieldAliasFactory: FieldAliasFacotry, private readonly fieldAliasFactory: FieldAliasFactory,
private readonly relationFieldAliasFactory: RelationFieldAliasFactory, private readonly relationFieldAliasFactory: RelationFieldAliasFactory,
) {} ) {}

View File

@ -12,6 +12,11 @@ import { FieldsStringFactory } from 'src/workspace/workspace-query-builder/facto
import { ArgsAliasFactory } from 'src/workspace/workspace-query-builder/factories/args-alias.factory'; import { ArgsAliasFactory } from 'src/workspace/workspace-query-builder/factories/args-alias.factory';
import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util'; import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util';
export interface UpdateManyQueryFactoryOptions
extends WorkspaceQueryBuilderOptions {
atMost?: number;
}
@Injectable() @Injectable()
export class UpdateManyQueryFactory { export class UpdateManyQueryFactory {
constructor( constructor(
@ -24,7 +29,7 @@ export class UpdateManyQueryFactory {
Filter extends RecordFilter = RecordFilter, Filter extends RecordFilter = RecordFilter,
>( >(
args: UpdateManyResolverArgs<Record, Filter>, args: UpdateManyResolverArgs<Record, Filter>,
options: WorkspaceQueryBuilderOptions, options: UpdateManyQueryFactoryOptions,
) { ) {
const fieldsString = await this.fieldsStringFactory.create( const fieldsString = await this.fieldsStringFactory.create(
options.info, options.info,
@ -47,6 +52,7 @@ export class UpdateManyQueryFactory {
update${computeObjectTargetTable(options.objectMetadataItem)}Collection( update${computeObjectTargetTable(options.objectMetadataItem)}Collection(
set: ${stringifyWithoutKeyQuote(argsData)}, set: ${stringifyWithoutKeyQuote(argsData)},
filter: ${stringifyWithoutKeyQuote(args.filter)}, filter: ${stringifyWithoutKeyQuote(args.filter)},
atMost: ${options.atMost ?? 1},
) { ) {
affectedCount affectedCount
records { records {

View File

@ -21,8 +21,14 @@ import { FindOneQueryFactory } from './factories/find-one-query.factory';
import { CreateManyQueryFactory } from './factories/create-many-query.factory'; import { CreateManyQueryFactory } from './factories/create-many-query.factory';
import { UpdateOneQueryFactory } from './factories/update-one-query.factory'; import { UpdateOneQueryFactory } from './factories/update-one-query.factory';
import { DeleteOneQueryFactory } from './factories/delete-one-query.factory'; import { DeleteOneQueryFactory } from './factories/delete-one-query.factory';
import { UpdateManyQueryFactory } from './factories/update-many-query.factory'; import {
import { DeleteManyQueryFactory } from './factories/delete-many-query.factory'; UpdateManyQueryFactory,
UpdateManyQueryFactoryOptions,
} from './factories/update-many-query.factory';
import {
DeleteManyQueryFactory,
DeleteManyQueryFactoryOptions,
} from './factories/delete-many-query.factory';
@Injectable() @Injectable()
export class WorkspaceQueryBuilderFactory { export class WorkspaceQueryBuilderFactory {
@ -81,14 +87,14 @@ export class WorkspaceQueryBuilderFactory {
Filter extends RecordFilter = RecordFilter, Filter extends RecordFilter = RecordFilter,
>( >(
args: UpdateManyResolverArgs<Record, Filter>, args: UpdateManyResolverArgs<Record, Filter>,
options: WorkspaceQueryBuilderOptions, options: UpdateManyQueryFactoryOptions,
): Promise<string> { ): Promise<string> {
return this.updateManyQueryFactory.create(args, options); return this.updateManyQueryFactory.create(args, options);
} }
deleteMany<Filter extends RecordFilter = RecordFilter>( deleteMany<Filter extends RecordFilter = RecordFilter>(
args: DeleteManyResolverArgs<Filter>, args: DeleteManyResolverArgs<Filter>,
options: WorkspaceQueryBuilderOptions, options: DeleteManyQueryFactoryOptions,
): Promise<string> { ): Promise<string> {
return this.deleteManyQueryFactory.create(args, options); return this.deleteManyQueryFactory.create(args, options);
} }

View File

@ -0,0 +1,34 @@
import {
BadRequestException,
HttpException,
InternalServerErrorException,
} from '@nestjs/common';
interface PgGraphQLErrorMapping {
[key: string]: (command: string, objectName: string) => HttpException;
}
const pgGraphQLErrorMapping: PgGraphQLErrorMapping = {
'delete impacts too many records': (command, objectName) =>
new BadRequestException(
`Cannot ${command} ${objectName} because it impacts too many records.`,
),
};
export const computePgGraphQLError = (
command: string,
objectName: string,
errors: any[],
) => {
const error = errors[0];
const errorMessage = error?.message;
const mappedError = pgGraphQLErrorMapping[errorMessage];
if (mappedError) {
return mappedError(command, objectName);
}
return new InternalServerErrorException(
`GraphQL errors on ${command}${objectName}: ${JSON.stringify(error)}`,
);
};

View File

@ -1,9 +1,4 @@
import { import { BadRequestException, Inject, Injectable } from '@nestjs/common';
BadRequestException,
Inject,
Injectable,
InternalServerErrorException,
} from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { IConnection } from 'src/utils/pagination/interfaces/connection.interface'; import { IConnection } from 'src/utils/pagination/interfaces/connection.interface';
@ -39,12 +34,14 @@ import { ObjectRecordDeleteEvent } from 'src/integrations/event-emitter/types/ob
import { ObjectRecordCreateEvent } from 'src/integrations/event-emitter/types/object-record-create.event'; import { ObjectRecordCreateEvent } from 'src/integrations/event-emitter/types/object-record-create.event';
import { ObjectRecordUpdateEvent } from 'src/integrations/event-emitter/types/object-record-update.event'; import { ObjectRecordUpdateEvent } from 'src/integrations/event-emitter/types/object-record-update.event';
import { WorkspacePreQueryHookService } from 'src/workspace/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service'; import { WorkspacePreQueryHookService } from 'src/workspace/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface'; import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
import { import {
PGGraphQLMutation, PGGraphQLMutation,
PGGraphQLResult, PGGraphQLResult,
} from './interfaces/pg-graphql.interface'; } from './interfaces/pg-graphql.interface';
import { computePgGraphQLError } from './utils/compute-pg-graphql-error.util';
@Injectable() @Injectable()
export class WorkspaceQueryRunnerService { export class WorkspaceQueryRunnerService {
@ -55,6 +52,7 @@ export class WorkspaceQueryRunnerService {
private readonly messageQueueService: MessageQueueService, private readonly messageQueueService: MessageQueueService,
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly workspacePreQueryHookService: WorkspacePreQueryHookService, private readonly workspacePreQueryHookService: WorkspacePreQueryHookService,
private readonly environmentService: EnvironmentService,
) {} ) {}
async findMany< async findMany<
@ -218,10 +216,12 @@ export class WorkspaceQueryRunnerService {
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> { ): Promise<Record[] | undefined> {
const { workspaceId, objectMetadataItem } = options; const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.updateMany( const maximumRecordAffected =
args, this.environmentService.getMutationMaximumRecordAffected();
options, const query = await this.workspaceQueryBuilderFactory.updateMany(args, {
); ...options,
atMost: maximumRecordAffected,
});
const result = await this.execute(query, workspaceId); const result = await this.execute(query, workspaceId);
@ -248,10 +248,12 @@ export class WorkspaceQueryRunnerService {
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> { ): Promise<Record[] | undefined> {
const { workspaceId, objectMetadataItem } = options; const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.deleteMany( const maximumRecordAffected =
args, this.environmentService.getMutationMaximumRecordAffected();
options, const query = await this.workspaceQueryBuilderFactory.deleteMany(args, {
); ...options,
atMost: maximumRecordAffected,
});
const result = await this.execute(query, workspaceId); const result = await this.execute(query, workspaceId);
@ -337,16 +339,16 @@ export class WorkspaceQueryRunnerService {
); );
await workspaceDataSource?.query(` await workspaceDataSource?.query(`
SET search_path TO ${this.workspaceDataSourceService.getSchemaName( SET search_path TO ${this.workspaceDataSourceService.getSchemaName(
workspaceId, workspaceId,
)}; )};
`); `);
const results = await workspaceDataSource?.query<PGGraphQLResult>(` const results = await workspaceDataSource?.query<PGGraphQLResult>(`
SELECT graphql.resolve($$ SELECT graphql.resolve($$
${query} ${query}
$$); $$);
`); `);
return results; return results;
} }
@ -363,11 +365,13 @@ export class WorkspaceQueryRunnerService {
const errors = graphqlResult?.[0]?.resolve?.errors; const errors = graphqlResult?.[0]?.resolve?.errors;
if (errors && errors.length > 0) { if (errors && errors.length > 0) {
throw new InternalServerErrorException( const error = computePgGraphQLError(
`GraphQL errors on ${command}${ command,
objectMetadataItem.nameSingular objectMetadataItem.nameSingular,
}: ${JSON.stringify(errors)}`, errors,
); );
throw error;
} }
return parseResult(result); return parseResult(result);