From f556e64ae28d06139fd871d855b7611f9906cb66 Mon Sep 17 00:00:00 2001 From: SkyHuang <906268297@qq.com> Date: Fri, 10 Jan 2025 19:01:25 +0800 Subject: [PATCH] fix: database execution error message (#1236) * fix: database execution error message * fix: db excute error for add records * fix: record history e2e testing --- .../src/event-emitter/events/event.enum.ts | 2 + .../listeners/record-history.listener.ts | 6 ++ .../src/features/calculation/batch.service.ts | 4 +- .../field-converting.service.ts | 6 +- .../src/features/field/field.service.ts | 11 ++-- .../src/features/record/record.service.ts | 6 +- .../test/record-history.e2e-spec.ts | 4 +- packages/db-main-prisma/src/prisma.service.ts | 66 +++++++------------ 8 files changed, 51 insertions(+), 54 deletions(-) diff --git a/apps/nestjs-backend/src/event-emitter/events/event.enum.ts b/apps/nestjs-backend/src/event-emitter/events/event.enum.ts index d24d071699..d328545889 100644 --- a/apps/nestjs-backend/src/event-emitter/events/event.enum.ts +++ b/apps/nestjs-backend/src/event-emitter/events/event.enum.ts @@ -63,4 +63,6 @@ export enum Events { WORKFLOW_DEACTIVATE = 'workflow.deactivate', CROP_IMAGE = 'crop.image', + + RECORD_HISTORY_CREATE = 'record.history.create', } diff --git a/apps/nestjs-backend/src/event-emitter/listeners/record-history.listener.ts b/apps/nestjs-backend/src/event-emitter/listeners/record-history.listener.ts index 752a04d032..33307c9616 100644 --- a/apps/nestjs-backend/src/event-emitter/listeners/record-history.listener.ts +++ b/apps/nestjs-backend/src/event-emitter/listeners/record-history.listener.ts @@ -9,6 +9,7 @@ import { Knex } from 'knex'; import { isString } from 'lodash'; import { InjectModel } from 'nest-knexjs'; import { BaseConfig, IBaseConfig } from '../../configs/base.config'; +import { EventEmitterService } from '../event-emitter.service'; import { Events, RecordUpdateEvent } from '../events'; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -18,6 +19,7 @@ const SELECT_FIELD_TYPE_SET = new Set([FieldType.SingleSelect, FieldType.Multipl export class RecordHistoryListener { constructor( private readonly prismaService: PrismaService, + private readonly eventEmitterService: EventEmitterService, @BaseConfig() private readonly baseConfig: IBaseConfig, @InjectModel('CUSTOM_KNEX') private readonly knex: Knex ) {} @@ -130,6 +132,10 @@ export class RecordHistoryListener { await this.prismaService.$executeRawUnsafe(query); } + + this.eventEmitterService.emit(Events.RECORD_HISTORY_CREATE, { + recordIds: records.map((record) => record.id), + }); } private minimizeFieldOptions( diff --git a/apps/nestjs-backend/src/features/calculation/batch.service.ts b/apps/nestjs-backend/src/features/calculation/batch.service.ts index df050bcb8f..3190afa74e 100644 --- a/apps/nestjs-backend/src/features/calculation/batch.service.ts +++ b/apps/nestjs-backend/src/features/calculation/batch.service.ts @@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import type { IOtOperation } from '@teable/core'; import { IdPrefix, RecordOpBuilder } from '@teable/core'; -import { PrismaService } from '@teable/db-main-prisma'; +import { PrismaService, wrapWithValidationErrorHandler } from '@teable/db-main-prisma'; import { Knex } from 'knex'; import { groupBy, isEmpty, keyBy } from 'lodash'; import { customAlphabet } from 'nanoid'; @@ -246,7 +246,7 @@ export class BatchService { await prisma.$executeRawUnsafe(insertTempTableSql); // 3.update data - await prisma.$executeRawUnsafe(updateRecordSql); + await wrapWithValidationErrorHandler(() => prisma.$executeRawUnsafe(updateRecordSql)); // 4.delete temporary table const dropTempTableSql = this.knex.schema.dropTable(tempTableName).toQuery(); diff --git a/apps/nestjs-backend/src/features/field/field-calculate/field-converting.service.ts b/apps/nestjs-backend/src/features/field/field-calculate/field-converting.service.ts index 883acf7ecf..3774ea98a3 100644 --- a/apps/nestjs-backend/src/features/field/field-calculate/field-converting.service.ts +++ b/apps/nestjs-backend/src/features/field/field-calculate/field-converting.service.ts @@ -22,7 +22,7 @@ import { PRIMARY_SUPPORTED_TYPES, RecordOpBuilder, } from '@teable/core'; -import { PrismaService } from '@teable/db-main-prisma'; +import { PrismaService, wrapWithValidationErrorHandler } from '@teable/db-main-prisma'; import { Knex } from 'knex'; import { difference, intersection, isEmpty, isEqual, keyBy, set } from 'lodash'; import { InjectModel } from 'nest-knexjs'; @@ -1134,7 +1134,9 @@ export class FieldConvertingService { if (notNull) table.dropNullable(dbFieldName); }) .toQuery(); - await this.prismaService.txClient().$executeRawUnsafe(fieldValidationQuery); + await wrapWithValidationErrorHandler(() => + this.prismaService.txClient().$executeRawUnsafe(fieldValidationQuery) + ); } async closeConstraint(tableId: string, newField: IFieldInstance, oldField: IFieldInstance) { diff --git a/apps/nestjs-backend/src/features/field/field.service.ts b/apps/nestjs-backend/src/features/field/field.service.ts index 58cb006260..1e55e18d7c 100644 --- a/apps/nestjs-backend/src/features/field/field.service.ts +++ b/apps/nestjs-backend/src/features/field/field.service.ts @@ -19,7 +19,7 @@ import { checkFieldValidationEnabled, } from '@teable/core'; import type { Field as RawField, Prisma } from '@teable/db-main-prisma'; -import { PrismaService } from '@teable/db-main-prisma'; +import { PrismaService, wrapWithValidationErrorHandler } from '@teable/db-main-prisma'; import { instanceToPlain } from 'class-transformer'; import { Knex } from 'knex'; import { keyBy, sortBy } from 'lodash'; @@ -534,7 +534,9 @@ export class FieldService implements IReadonlyAdapterService { } if (key === 'dbFieldType') { - await this.alterTableModifyFieldType(fieldId, newValue as DbFieldType); + await wrapWithValidationErrorHandler(() => + this.alterTableModifyFieldType(fieldId, newValue as DbFieldType) + ); } if (key === 'dbFieldName') { @@ -542,8 +544,9 @@ export class FieldService implements IReadonlyAdapterService { } if (key === 'unique' || key === 'notNull') { - console.log('alterTableModifyFieldValidation', fieldId, { [key]: newValue }); - await this.alterTableModifyFieldValidation(fieldId, key, newValue as boolean | undefined); + await wrapWithValidationErrorHandler(() => + this.alterTableModifyFieldValidation(fieldId, key, newValue as boolean | undefined) + ); } return { [key]: newValue ?? null }; diff --git a/apps/nestjs-backend/src/features/record/record.service.ts b/apps/nestjs-backend/src/features/record/record.service.ts index 1f7f147b72..6a468e9985 100644 --- a/apps/nestjs-backend/src/features/record/record.service.ts +++ b/apps/nestjs-backend/src/features/record/record.service.ts @@ -36,7 +36,7 @@ import { Relationship, } from '@teable/core'; import type { Prisma } from '@teable/db-main-prisma'; -import { PrismaService } from '@teable/db-main-prisma'; +import { PrismaService, wrapWithValidationErrorHandler } from '@teable/db-main-prisma'; import type { ICreateRecordsRo, IGetRecordQuery, @@ -1048,7 +1048,9 @@ export class RecordService { const sql = this.dbProvider.batchInsertSql(dbTableName, snapshots); - await this.prismaService.txClient().$executeRawUnsafe(sql); + await wrapWithValidationErrorHandler(() => + this.prismaService.txClient().$executeRawUnsafe(sql) + ); return snapshots; } diff --git a/apps/nestjs-backend/test/record-history.e2e-spec.ts b/apps/nestjs-backend/test/record-history.e2e-spec.ts index 7d6b8243ca..846620e2cc 100644 --- a/apps/nestjs-backend/test/record-history.e2e-spec.ts +++ b/apps/nestjs-backend/test/record-history.e2e-spec.ts @@ -31,11 +31,11 @@ describe('Record history (e2e)', () => { const baseConfigService = app.get(baseConfig.KEY) as IBaseConfig; baseConfigService.recordHistoryDisabled = false; - awaitWithEvent = createAwaitWithEvent(eventEmitterService, Events.TABLE_RECORD_UPDATE); + awaitWithEvent = createAwaitWithEvent(eventEmitterService, Events.RECORD_HISTORY_CREATE); }); afterAll(async () => { - eventEmitterService.eventEmitter.removeAllListeners(Events.TABLE_RECORD_UPDATE); + eventEmitterService.eventEmitter.removeAllListeners(Events.RECORD_HISTORY_CREATE); await app.close(); }); diff --git a/packages/db-main-prisma/src/prisma.service.ts b/packages/db-main-prisma/src/prisma.service.ts index 63b9004d43..cd40667cca 100644 --- a/packages/db-main-prisma/src/prisma.service.ts +++ b/packages/db-main-prisma/src/prisma.service.ts @@ -13,47 +13,30 @@ interface ITx { rawOpMaps?: unknown; } -function proxyClient(tx: Prisma.TransactionClient) { - return new Proxy(tx, { - get(target, p) { - if (p === '$queryRawUnsafe' || p === '$executeRawUnsafe') { - return async function (query: string, ...args: unknown[]) { - try { - return await target[p](query, ...args); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - const code = e.meta?.code ?? e.code; - if ( - code === PostgresErrorCode.UNIQUE_VIOLATION || - code === SqliteErrorCode.UNIQUE_VIOLATION - ) { - throw new HttpException( - 'Duplicate detected! Please ensure that all fields with unique value validation are indeed unique.', - HttpStatus.BAD_REQUEST - ); - } - if ( - code === PostgresErrorCode.NOT_NULL_VIOLATION || - code === SqliteErrorCode.NOT_NULL_VIOLATION - ) { - throw new HttpException( - 'One or more required fields were not provided! Please ensure all mandatory fields are filled.', - HttpStatus.BAD_REQUEST - ); - } - throw new HttpException( - `An error occurred in ${p}: ${e.message}`, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } - }; - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return target[p]; - }, - }); -} +export const wrapWithValidationErrorHandler = async (fn: () => Promise) => { + try { + await fn(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + const code = e.meta?.code ?? e.code; + if (code === PostgresErrorCode.UNIQUE_VIOLATION || code === SqliteErrorCode.UNIQUE_VIOLATION) { + throw new HttpException( + 'Duplicate detected! Please ensure that all fields with unique value validation are indeed unique.', + HttpStatus.BAD_REQUEST + ); + } + if ( + code === PostgresErrorCode.NOT_NULL_VIOLATION || + code === SqliteErrorCode.NOT_NULL_VIOLATION + ) { + throw new HttpException( + 'One or more required fields were not provided! Please ensure all mandatory fields are filled.', + HttpStatus.BAD_REQUEST + ); + } + throw new HttpException(`An error occurred: ${e.message}`, HttpStatus.INTERNAL_SERVER_ERROR); + } +}; @Injectable() export class PrismaService @@ -118,7 +101,6 @@ export class PrismaService await this.cls.runWith(this.cls.get(), async () => { result = await super.$transaction(async (prisma) => { - prisma = proxyClient(prisma); this.cls.set('tx.client', prisma); this.cls.set('tx.id', nanoid()); this.cls.set('tx.timeStr', new Date().toISOString());