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:
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>) => {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
) {}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)}`,
|
||||
);
|
||||
};
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user