import { db, Prisma, PrismaClient } from '@nice/common'; import { Operations, DelegateArgs, DelegateReturnTypes, DataArgs, WhereArgs, DelegateFuncs, UpdateOrderArgs, TransactionType, OrderByArgs, SelectArgs, } from './base.type'; import { NotFoundException, InternalServerErrorException, } from '@nestjs/common'; import { ERROR_MAP, operationT, PrismaErrorCode } from './errorMap.prisma'; /** * BaseService provides a generic CRUD interface for a prisma model. * It enables common data operations such as find, create, update, and delete. * * @template D - Type for the model delegate, defining available operations. * @template A - Arguments for the model delegate's operations. * @template R - Return types for the model delegate's operations. */ export class BaseService< D extends DelegateFuncs, A extends DelegateArgs = DelegateArgs, R extends DelegateReturnTypes = DelegateReturnTypes, > { protected ORDER_INTERVAL = 100; /** * Initializes the BaseService with the specified model. * @param model - The Prisma model delegate for database operations. */ constructor( protected prisma: PrismaClient, protected objectType: string, protected enableOrder: boolean = false, ) {} /** * Retrieves the name of the model dynamically. * @returns {string} - The name of the model. */ private getModelName(): string { const modelName = this.getModel().constructor.name; return modelName; } private getModel(tx?: TransactionType): D { return tx?.[this.objectType] || (this.prisma[this.objectType] as D); } /** * Error handling helper function */ private handleError(error: any, operation: operationT): never { if (error instanceof Prisma.PrismaClientKnownRequestError) { const handler = ERROR_MAP[error.code as PrismaErrorCode]; if (handler) { throw handler( operation, error?.meta || { target: 'record', model: this.getModelName(), }, ); } throw new InternalServerErrorException( `Database error: ${error.message}`, ); } throw new InternalServerErrorException( `Unexpected error: ${error.message || 'Unknown error occurred.'}`, ); } /** * Finds a unique record by given criteria. * @param args - Arguments to find a unique record. * @returns {Promise} - A promise resolving to the found record. * @example * const user = await service.findUnique({ where: { id: 'user_id' } }); */ async findUnique(args: A['findUnique']): Promise { try { return this.getModel().findUnique(args as any) as Promise< R['findUnique'] >; } catch (error) { this.handleError(error, 'read'); } } /** * Finds the first record matching the given criteria. * @param args - Arguments to find the first matching record. * @returns {Promise} - A promise resolving to the first matching record. * @example * const firstUser = await service.findFirst({ where: { name: 'John' } }); */ async findFirst(args: A['findFirst']): Promise { try { return this.getModel().findFirst(args as any) as Promise; } catch (error) { this.handleError(error, 'read'); } } /** * Finds a record by its ID. * @param id - The ID of the record to find. * @param args - Optional additional arguments for the find operation. * @returns {Promise} - A promise resolving to the found record. * @throws {NotFoundException} - If no record is found with the given ID. * @example * const user = await service.findById('user_id'); */ async findById(id: string, args?: A['findFirst']): Promise { try { const record = (await this.getModel().findFirst({ where: { id }, ...(args || {}), })) as R['findFirst']; if (!record) { throw new NotFoundException(`Record with ID ${id} not found.`); } return record; } catch (error) { this.handleError(error, 'read'); } } /** * Finds multiple records matching the given criteria. * @param args - Arguments to find multiple records. * @returns {Promise} - A promise resolving to the list of found records. * @example * const users = await service.findMany({ where: { isActive: true } }); */ async findMany(args: A['findMany']): Promise { try { return this.getModel().findMany(args as any) as Promise; } catch (error) { this.handleError(error, 'read'); } } /** * Creates a new record with the given data. * @param args - Arguments to create a record. * @returns {Promise} - A promise resolving to the created record. * @example * const newUser = await service.create({ data: { name: 'John Doe' } }); */ async create(args: A['create'], params?: any): Promise { try { if (this.enableOrder && !(args as any).data.order) { // 查找当前最大的 order 值 const maxOrderItem = (await this.getModel(params?.tx).findFirst({ orderBy: { order: 'desc' }, })) as any; // 设置新记录的 order 值 const newOrder = maxOrderItem ? maxOrderItem.order + this.ORDER_INTERVAL : 1; // 将 order 添加到创建参数中 (args as any).data.order = newOrder; } return this.getModel(params?.tx).create(args as any) as Promise< R['create'] >; } catch (error) { this.handleError(error, 'create'); } } /** * Creates multiple new records with the given data. * @param args - Arguments to create multiple records. * @returns {Promise} - A promise resolving to the created records. * @example * const newUsers = await service.createMany({ data: [{ name: 'John' }, { name: 'Jane' }] }); */ async createMany( args: A['createMany'], params?: any, ): Promise { try { return this.getModel(params?.tx).createMany(args as any) as Promise< R['createMany'] >; } catch (error) { this.handleError(error, 'create'); } } /** * Updates a record with the given data. * @param args - Arguments to update a record. * @returns {Promise} - A promise resolving to the updated record. * @example * const updatedUser = await service.update({ where: { id: 'user_id' }, data: { name: 'John' } }); */ async update(args: A['update'], params?: any): Promise { try { return this.getModel(params?.tx).update(args as any) as Promise< R['update'] >; } catch (error) { this.handleError(error, 'update'); } } /** * Updates a record by ID with the given data. * @param id - The ID of the record to update. * @param data - The data to update the record with. * @returns {Promise} - A promise resolving to the updated record. * @example * const updatedUser = await service.updateById('user_id', { name: 'John Doe' }); */ async updateById( id: string, data: DataArgs, ): Promise { try { return (await this.getModel().update({ where: { id }, data: data as any, })) as R['update']; } catch (error) { this.handleError(error, 'update'); } } /** * Deletes a record by ID. * @param id - The ID of the record to delete. * @returns {Promise} - A promise resolving to the deleted record. * @example * const deletedUser = await service.deleteById('user_id'); */ async deleteById(id: string): Promise { try { return (await this.getModel().delete({ where: { id }, })) as R['delete']; } catch (error) { this.handleError(error, 'delete'); } } /** * Deletes a record based on the given criteria. * @param args - Arguments to delete a record. * @returns {Promise} - A promise resolving to the deleted record. * @example * const deletedUser = await service.delete({ where: { name: 'John' } }); */ async delete(args: A['delete'], params?: any): Promise { try { return this.getModel(params?.tx).delete(args as any) as Promise< R['delete'] >; } catch (error) { this.handleError(error, 'delete'); } } /** * Creates or updates a record based on the given criteria. * @param args - Arguments to upsert a record. * @returns {Promise} - A promise resolving to the created or updated record. * @example * const user = await service.upsert({ where: { id: 'user_id' }, create: { name: 'John' }, update: { name: 'Johnny' } }); */ async upsert(args: A['upsert']): Promise { try { return this.getModel().upsert(args as any) as Promise; } catch (error) { this.handleError(error, 'create'); } } /** * Counts the number of records matching the given criteria. * @param args - Arguments to count records. * @returns {Promise} - A promise resolving to the count. * @example * const userCount = await service.count({ where: { isActive: true } }); */ async count(args: A['count']): Promise { try { return this.getModel().count(args as any) as Promise; } catch (error) { this.handleError(error, 'read'); } } /** * Aggregates records based on the given criteria. * @param args - Arguments to aggregate records. * @returns {Promise} - A promise resolving to the aggregation result. * @example * const userAggregates = await service.aggregate({ _count: true }); */ async aggregate(args: A['aggregate']): Promise { try { return this.getModel().aggregate(args as any) as Promise; } catch (error) { this.handleError(error, 'read'); } } /** * Deletes multiple records based on the given criteria. * @param args - Arguments to delete multiple records. * @returns {Promise} - A promise resolving to the result of the deletion. * @example * const deleteResult = await service.deleteMany({ where: { isActive: false } }); */ async deleteMany( args: A['deleteMany'], params?: any, ): Promise { try { return this.getModel(params?.tx).deleteMany(args as any) as Promise< R['deleteMany'] >; } catch (error) { this.handleError(error, 'delete'); } } /** * Updates multiple records based on the given criteria. * @param args - Arguments to update multiple records. * @returns {Promise} - A promise resolving to the result of the update. * @example * const updateResult = await service.updateMany({ where: { isActive: true }, data: { isActive: false } }); */ async updateMany(args: A['updateMany']): Promise { try { return this.getModel().updateMany(args as any) as Promise< R['updateMany'] >; } catch (error) { this.handleError(error, 'update'); } } /** * Finds a record by unique criteria or creates it if not found. * @param args - Arguments to find or create a record. * @returns {Promise} - A promise resolving to the found or created record. * @example * const user = await service.findOrCreate({ where: { email: 'john@example.com' }, create: { email: 'john@example.com', name: 'John' } }); */ async findOrCreate(args: { where: WhereArgs; create: DataArgs; }): Promise { try { const existing = (await this.getModel().findUnique({ where: args.where, } as any)) as R['findUnique']; if (existing) { return existing; } return this.getModel().create({ data: args.create } as any) as Promise< R['create'] >; } catch (error) { this.handleError(error, 'create'); } } /** * Checks if a record exists based on the given criteria. * @param where - The criteria to check for existence. * @returns {Promise} - A promise resolving to true if the record exists, false otherwise. * @example * const exists = await service.exists({ email: 'john@example.com' }); */ async exists(where: WhereArgs): Promise { try { const count = (await this.getModel().count({ where } as any)) as number; return count > 0; } catch (error) { this.handleError(error, 'read'); } } /** * Soft deletes records by setting `isDeleted` to true for the given IDs. * @param ids - An array of IDs of the records to soft delete. * @param data - Additional data to update on soft delete. (Optional) * @returns {Promise} - A promise resolving to an array of updated records. * @example * const softDeletedUsers = await service.softDeleteByIds(['user_id1', 'user_id2'], { reason: 'Bulk deletion' }); */ async softDeleteByIds( ids: string[], data: Partial> = {}, // Default to empty object ): Promise { try { if (!ids || ids.length === 0) { return []; // Return empty array if no IDs are provided } return this.getModel().updateMany({ where: { id: { in: ids } }, data: { ...data, deletedAt: new Date() } as any, }) as Promise; } catch (error) { this.handleError(error, 'delete'); } } /** * Restores soft-deleted records by setting `isDeleted` to false for the given IDs. * @param ids - An array of IDs of the records to restore. * @param data - Additional data to update on restore. (Optional) * @returns {Promise} - A promise resolving to an array of updated records. * @example * const restoredUsers = await service.restoreByIds(['user_id1', 'user_id2'], { restoredBy: 'admin' }); */ async restoreByIds( ids: string[], data: Partial> = {}, // Default to empty object ): Promise { try { if (!ids || ids.length === 0) { return []; // Return empty array if no IDs are provided } return this.getModel().updateMany({ where: { id: { in: ids }, isDeleted: true }, // Only restore soft-deleted records data: { ...data, deletedAt: null } as any, }) as Promise; } catch (error) { this.handleError(error, 'update'); } } /** * Finds multiple records with pagination. * @param args - Arguments including page, pageSize, and optional filters. * @returns {Promise} - A promise resolving to the paginated list of records. * @example * const users = await service.findManyWithPagination({ page: 1, pageSize: 10, where: { isActive: true } }); */ async findManyWithPagination(args: { page?: number; pageSize?: number; where?: WhereArgs; orderBy?: OrderByArgs; select?: SelectArgs; }): Promise<{ items: R['findMany']; totalPages: number }> { const { page = 1, pageSize = 10, where, select, orderBy } = args; try { // 获取总记录数 const total = (await this.getModel().count({ where })) as number; // 获取分页数据 const items = (await this.getModel().findMany({ where, select, orderBy, skip: (page - 1) * pageSize, take: pageSize, } as any)) as R['findMany']; // 计算总页数 const totalPages = Math.ceil(total / pageSize); return { items, totalPages, }; } catch (error) { this.handleError(error, 'read'); } } /** * 基于游标的分页查询方法 * @description 该方法实现了基于游标的分页查询,相比传统的offset/limit分页有更好的性能 * @param args 查询参数对象,包含cursor、take、where、orderBy、select等字段 * @returns 返回查询结果对象,包含items数组、总数和下一页游标 */ async findManyWithCursor( args: A['findMany'], ): Promise<{ items: R['findMany']; nextCursor: string | null }> { // 解构查询参数,设置默认每页取10条记录 const { cursor, take = 6, where, orderBy, select } = args as any; try { const items = (await this.getModel().findMany({ where: where, orderBy: [{ ...orderBy }, { updatedAt: 'desc' }, { id: 'desc' }], select, take: take + 1, cursor: cursor ? { updatedAt: cursor.split('_')[0], id: cursor.split('_')[1] } : undefined, } as any)) as any[]; /** * 处理下一页游标 * @description * 1. 如果查到的记录数超过take,说明还有下一页 * 2. 将最后一条记录弹出,用其updatedAt和id构造下一页游标 * 3. 游标格式为: updatedAt_id */ let nextCursor: string | null = ''; if (items.length > take) { const nextItem = items.pop(); nextCursor = `${nextItem!.updatedAt?.toISOString()}_${nextItem!.id}`; } if (nextCursor === '') { nextCursor = null; } /** * 返回查询结果 * @returns {Object} * - items: 当前页记录 * - totalCount: 总记录数 * - nextCursor: 下一页游标 */ return { items: items as R['findMany'], nextCursor: nextCursor, }; } catch (error) { this.handleError(error, 'read'); } } async updateOrder(args: UpdateOrderArgs) { const { id, overId } = args; const [currentObject, targetObject] = (await Promise.all([ this.findFirst({ where: { id } } as any), this.findFirst({ where: { id: overId } } as any), ])) as any; if (!currentObject || !targetObject) { throw new Error('Invalid object or target object'); } const nextObject = (await this.findFirst({ where: { order: { gt: targetObject.order }, deletedAt: null, }, orderBy: { order: 'asc' }, } as any)) as any; const newOrder = nextObject ? (targetObject.order + nextObject.order) / 2 : targetObject.order + this.ORDER_INTERVAL; return this.update({ where: { id }, data: { order: newOrder } } as any); } /** * Wraps the result of a database operation with a transformation function. * @template T - The type of the result to be transformed. * @param operationPromise - The promise representing the database operation. * @param transformFn - A function that transforms the result. * @returns {Promise} - A promise resolving to the transformed result. * @example * const user = await service.wrapResult( * service.findUnique({ where: { id: 'user_id' } }), * (result) => ({ ...result, fullName: `${result.firstName} ${result.lastName}` }) * ); */ async wrapResult( operationPromise: Promise, transformFn: (result: T) => Promise, ): Promise { try { const result = await operationPromise; return await transformFn(result); } catch (error) { throw error; // Re-throw the error to maintain existing error handling } } }