training_data/apps/server/src/models/base/base.service.ts

581 lines
19 KiB
TypeScript
Executable File

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<D> = DelegateArgs<D>,
R extends DelegateReturnTypes<D> = DelegateReturnTypes<D>,
> {
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<R['findUnique']>} - A promise resolving to the found record.
* @example
* const user = await service.findUnique({ where: { id: 'user_id' } });
*/
async findUnique(args: A['findUnique']): Promise<R['findUnique']> {
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<R['findFirst']>} - A promise resolving to the first matching record.
* @example
* const firstUser = await service.findFirst({ where: { name: 'John' } });
*/
async findFirst(args: A['findFirst']): Promise<R['findFirst']> {
try {
return this.getModel().findFirst(args as any) as Promise<R['findFirst']>;
} 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<R['findFirst']>} - 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<R['findFirst']> {
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<R['findMany']>} - A promise resolving to the list of found records.
* @example
* const users = await service.findMany({ where: { isActive: true } });
*/
async findMany(args: A['findMany']): Promise<R['findMany']> {
try {
return this.getModel().findMany(args as any) as Promise<R['findMany']>;
} catch (error) {
this.handleError(error, 'read');
}
}
/**
* Creates a new record with the given data.
* @param args - Arguments to create a record.
* @returns {Promise<R['create']>} - 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<R['create']> {
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<R['createMany']>} - 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<R['createMany']> {
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<R['update']>} - 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<R['update']> {
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<R['update']>} - A promise resolving to the updated record.
* @example
* const updatedUser = await service.updateById('user_id', { name: 'John Doe' });
*/
async updateById(
id: string,
data: DataArgs<A['update']>,
): Promise<R['update']> {
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<R['delete']>} - A promise resolving to the deleted record.
* @example
* const deletedUser = await service.deleteById('user_id');
*/
async deleteById(id: string): Promise<R['delete']> {
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<R['delete']>} - A promise resolving to the deleted record.
* @example
* const deletedUser = await service.delete({ where: { name: 'John' } });
*/
async delete(args: A['delete'], params?: any): Promise<R['delete']> {
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<R['upsert']>} - 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<R['upsert']> {
try {
return this.getModel().upsert(args as any) as Promise<R['upsert']>;
} catch (error) {
this.handleError(error, 'create');
}
}
/**
* Counts the number of records matching the given criteria.
* @param args - Arguments to count records.
* @returns {Promise<R['count']>} - A promise resolving to the count.
* @example
* const userCount = await service.count({ where: { isActive: true } });
*/
async count(args: A['count']): Promise<R['count']> {
try {
return this.getModel().count(args as any) as Promise<R['count']>;
} catch (error) {
this.handleError(error, 'read');
}
}
/**
* Aggregates records based on the given criteria.
* @param args - Arguments to aggregate records.
* @returns {Promise<R['aggregate']>} - A promise resolving to the aggregation result.
* @example
* const userAggregates = await service.aggregate({ _count: true });
*/
async aggregate(args: A['aggregate']): Promise<R['aggregate']> {
try {
return this.getModel().aggregate(args as any) as Promise<R['aggregate']>;
} catch (error) {
this.handleError(error, 'read');
}
}
/**
* Deletes multiple records based on the given criteria.
* @param args - Arguments to delete multiple records.
* @returns {Promise<R['deleteMany']>} - 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<R['deleteMany']> {
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<R['updateMany']>} - 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<R['updateMany']> {
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<R['findUnique'] | R['create']>} - 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<A['findUnique']>;
create: DataArgs<A['create']>;
}): Promise<R['findUnique'] | R['create']> {
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<boolean>} - 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<A['findUnique']>): Promise<boolean> {
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<R['update'][]>} - 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<DataArgs<A['update']>> = {}, // Default to empty object
): Promise<R['update'][]> {
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<R['update'][]>;
} 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<R['update'][]>} - 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<DataArgs<A['update']>> = {}, // Default to empty object
): Promise<R['update'][]> {
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<R['update'][]>;
} catch (error) {
this.handleError(error, 'update');
}
}
/**
* Finds multiple records with pagination.
* @param args - Arguments including page, pageSize, and optional filters.
* @returns {Promise<R['findMany']>} - 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<A['findMany']>;
orderBy?: OrderByArgs<A['findMany']>;
select?: SelectArgs<A['findMany']>;
}): 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<T>} - 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<T>(
operationPromise: Promise<T>,
transformFn: (result: T) => Promise<T>,
): Promise<T> {
try {
const result = await operationPromise;
return await transformFn(result);
} catch (error) {
throw error; // Re-throw the error to maintain existing error handling
}
}
}