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
# API_RATE_LIMITING_TTL=
# 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
# STORAGE_TYPE=local
# STORAGE_LOCAL_PATH=.local-storage
# MUTATION_MAXIMUM_RECORD_AFFECTED=100

View File

@ -299,4 +299,10 @@ export class EnvironmentService {
getApiRateLimitingLimit(): number {
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()
@IsBoolean()
IS_SIGN_UP_DISABLED?: boolean;
@CastToPositiveNumber()
@IsOptional()
@IsNumber()
MUTATION_MAXIMUM_RECORD_AFFECTED: number;
}
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';
export interface DeleteManyQueryFactoryOptions
extends WorkspaceQueryBuilderOptions {
atMost?: number;
}
@Injectable()
export class DeleteManyQueryFactory {
constructor(private readonly fieldsStringFactory: FieldsStringFactory) {}
async create(
args: DeleteManyResolverArgs,
options: WorkspaceQueryBuilderOptions,
options: DeleteManyQueryFactoryOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
@ -28,7 +33,7 @@ export class DeleteManyQueryFactory {
options.objectMetadataItem,
)}Collection(filter: ${stringifyWithoutKeyQuote(
args.filter,
)}, atMost: 30) {
)}, atMost: ${options.atMost ?? 1}) {
affectedCount
records {
${fieldsString}

View File

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

View File

@ -3,8 +3,8 @@ import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
@Injectable()
export class FieldAliasFacotry {
private readonly logger = new Logger(FieldAliasFacotry.name);
export class FieldAliasFactory {
private readonly logger = new Logger(FieldAliasFactory.name);
create(fieldKey: string, fieldMetadata: FieldMetadataInterface) {
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 { FieldAliasFacotry } from './field-alias.factory';
import { FieldAliasFactory } from './field-alias.factory';
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
@Injectable()
@ -17,7 +17,7 @@ export class FieldsStringFactory {
private readonly logger = new Logger(FieldsStringFactory.name);
constructor(
private readonly fieldAliasFactory: FieldAliasFacotry,
private readonly fieldAliasFactory: FieldAliasFactory,
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 { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util';
export interface UpdateManyQueryFactoryOptions
extends WorkspaceQueryBuilderOptions {
atMost?: number;
}
@Injectable()
export class UpdateManyQueryFactory {
constructor(
@ -24,7 +29,7 @@ export class UpdateManyQueryFactory {
Filter extends RecordFilter = RecordFilter,
>(
args: UpdateManyResolverArgs<Record, Filter>,
options: WorkspaceQueryBuilderOptions,
options: UpdateManyQueryFactoryOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
@ -47,6 +52,7 @@ export class UpdateManyQueryFactory {
update${computeObjectTargetTable(options.objectMetadataItem)}Collection(
set: ${stringifyWithoutKeyQuote(argsData)},
filter: ${stringifyWithoutKeyQuote(args.filter)},
atMost: ${options.atMost ?? 1},
) {
affectedCount
records {

View File

@ -21,8 +21,14 @@ import { FindOneQueryFactory } from './factories/find-one-query.factory';
import { CreateManyQueryFactory } from './factories/create-many-query.factory';
import { UpdateOneQueryFactory } from './factories/update-one-query.factory';
import { DeleteOneQueryFactory } from './factories/delete-one-query.factory';
import { UpdateManyQueryFactory } from './factories/update-many-query.factory';
import { DeleteManyQueryFactory } from './factories/delete-many-query.factory';
import {
UpdateManyQueryFactory,
UpdateManyQueryFactoryOptions,
} from './factories/update-many-query.factory';
import {
DeleteManyQueryFactory,
DeleteManyQueryFactoryOptions,
} from './factories/delete-many-query.factory';
@Injectable()
export class WorkspaceQueryBuilderFactory {
@ -81,14 +87,14 @@ export class WorkspaceQueryBuilderFactory {
Filter extends RecordFilter = RecordFilter,
>(
args: UpdateManyResolverArgs<Record, Filter>,
options: WorkspaceQueryBuilderOptions,
options: UpdateManyQueryFactoryOptions,
): Promise<string> {
return this.updateManyQueryFactory.create(args, options);
}
deleteMany<Filter extends RecordFilter = RecordFilter>(
args: DeleteManyResolverArgs<Filter>,
options: WorkspaceQueryBuilderOptions,
options: DeleteManyQueryFactoryOptions,
): Promise<string> {
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 {
BadRequestException,
Inject,
Injectable,
InternalServerErrorException,
} from '@nestjs/common';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
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 { 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 { EnvironmentService } from 'src/integrations/environment/environment.service';
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
import {
PGGraphQLMutation,
PGGraphQLResult,
} from './interfaces/pg-graphql.interface';
import { computePgGraphQLError } from './utils/compute-pg-graphql-error.util';
@Injectable()
export class WorkspaceQueryRunnerService {
@ -55,6 +52,7 @@ export class WorkspaceQueryRunnerService {
private readonly messageQueueService: MessageQueueService,
private readonly eventEmitter: EventEmitter2,
private readonly workspacePreQueryHookService: WorkspacePreQueryHookService,
private readonly environmentService: EnvironmentService,
) {}
async findMany<
@ -218,10 +216,12 @@ export class WorkspaceQueryRunnerService {
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.updateMany(
args,
options,
);
const maximumRecordAffected =
this.environmentService.getMutationMaximumRecordAffected();
const query = await this.workspaceQueryBuilderFactory.updateMany(args, {
...options,
atMost: maximumRecordAffected,
});
const result = await this.execute(query, workspaceId);
@ -248,10 +248,12 @@ export class WorkspaceQueryRunnerService {
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.deleteMany(
args,
options,
);
const maximumRecordAffected =
this.environmentService.getMutationMaximumRecordAffected();
const query = await this.workspaceQueryBuilderFactory.deleteMany(args, {
...options,
atMost: maximumRecordAffected,
});
const result = await this.execute(query, workspaceId);
@ -337,16 +339,16 @@ export class WorkspaceQueryRunnerService {
);
await workspaceDataSource?.query(`
SET search_path TO ${this.workspaceDataSourceService.getSchemaName(
workspaceId,
)};
`);
SET search_path TO ${this.workspaceDataSourceService.getSchemaName(
workspaceId,
)};
`);
const results = await workspaceDataSource?.query<PGGraphQLResult>(`
SELECT graphql.resolve($$
${query}
$$);
`);
SELECT graphql.resolve($$
${query}
$$);
`);
return results;
}
@ -363,11 +365,13 @@ export class WorkspaceQueryRunnerService {
const errors = graphqlResult?.[0]?.resolve?.errors;
if (errors && errors.length > 0) {
throw new InternalServerErrorException(
`GraphQL errors on ${command}${
objectMetadataItem.nameSingular
}: ${JSON.stringify(errors)}`,
const error = computePgGraphQLError(
command,
objectMetadataItem.nameSingular,
errors,
);
throw error;
}
return parseResult(result);