Last active
March 22, 2025 16:25
-
-
Save copy-ninja1/8a8353916b444e33f34d6ee56a48a9c2 to your computer and use it in GitHub Desktop.
Prisma Base Service with nestjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type Operations = | |
| 'aggregate' | |
| 'count' | |
| 'create' | |
| 'createMany' | |
| 'delete' | |
| 'deleteMany' | |
| 'findFirst' | |
| 'findMany' | |
| 'findUnique' | |
| 'update' | |
| 'updateMany' | |
| 'upsert'; | |
export type DelegateArgs<T> = { | |
[K in keyof T]: T[K] extends (args: infer A) => Promise<any> ? A : never; | |
}; | |
export type DelegateReturnTypes<T> = { | |
[K in keyof T]: T[K] extends (args: any) => Promise<infer R> ? R : never; | |
}; | |
export type WhereArgs<T> = T extends { where: infer W } ? W : never; | |
export type DataArgs<T> = T extends { data: infer D } ? D : never; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
BadRequestException, | |
NotFoundException, | |
ConflictException, | |
InternalServerErrorException, | |
} from '@nestjs/common'; | |
export const PrismaErrorCode = Object.freeze({ | |
P2000: 'P2000', | |
P2001: 'P2001', | |
P2002: 'P2002', | |
P2003: 'P2003', | |
P2006: 'P2006', | |
P2007: 'P2007', | |
P2008: 'P2008', | |
P2009: 'P2009', | |
P2010: 'P2010', | |
P2011: 'P2011', | |
P2012: 'P2012', | |
P2014: 'P2014', | |
P2015: 'P2015', | |
P2016: 'P2016', | |
P2017: 'P2017', | |
P2018: 'P2018', | |
P2019: 'P2019', | |
P2021: 'P2021', | |
P2023: 'P2023', | |
P2025: 'P2025', | |
P2031: 'P2031', | |
P2033: 'P2033', | |
P2034: 'P2034', | |
P2037: 'P2037', | |
P1000: 'P1000', | |
P1001: 'P1001', | |
P1002: 'P1002', | |
P1015: 'P1015', | |
P1017: 'P1017', | |
}); | |
export type PrismaErrorCode = keyof typeof PrismaErrorCode; | |
interface PrismaErrorMeta { | |
target?: string; | |
model?: string; | |
relationName?: string; | |
details?: string; | |
} | |
export type operationT = 'create' | 'read' | 'update' | 'delete'; | |
export type PrismaErrorHandler = ( | |
operation: operationT, | |
meta?: PrismaErrorMeta, | |
) => Error; | |
export const ERROR_MAP: Record<PrismaErrorCode, PrismaErrorHandler> = { | |
P2000: (_operation, meta) => | |
new BadRequestException( | |
`The provided value for ${meta?.target || 'a field'} is too long. Please use a shorter value.`, | |
), | |
P2001: (operation, meta) => | |
new NotFoundException( | |
`The ${meta?.model || 'record'} you are trying to ${operation} could not be found.`, | |
), | |
P2002: (operation, meta) => { | |
const field = meta?.target || 'unique field'; | |
switch (operation) { | |
case 'create': | |
return new ConflictException( | |
`A record with the same ${field} already exists. Please use a different value.`, | |
); | |
case 'update': | |
return new ConflictException( | |
`The new value for ${field} conflicts with an existing record.`, | |
); | |
default: | |
return new ConflictException( | |
`Unique constraint violation on ${field}.`, | |
); | |
} | |
}, | |
P2003: (operation) => | |
new BadRequestException( | |
`Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.`, | |
), | |
P2006: (_operation, meta) => | |
new BadRequestException( | |
`The provided value for ${meta?.target || 'a field'} is invalid. Please correct it.`, | |
), | |
P2007: (operation) => | |
new InternalServerErrorException( | |
`Data validation error during ${operation}. Please ensure all inputs are valid and try again.`, | |
), | |
P2008: (operation) => | |
new InternalServerErrorException( | |
`Failed to query the database during ${operation}. Please try again later.`, | |
), | |
P2009: (operation) => | |
new InternalServerErrorException( | |
`Invalid data fetched during ${operation}. Check query structure.`, | |
), | |
P2010: () => | |
new InternalServerErrorException( | |
`Invalid raw query. Ensure your query is correct and try again.`, | |
), | |
P2011: (_operation, meta) => | |
new BadRequestException( | |
`The required field ${meta?.target || 'a field'} is missing. Please provide it to continue.`, | |
), | |
P2012: (operation, meta) => | |
new BadRequestException( | |
`Missing required relation ${meta?.relationName || ''}. Ensure all related data exists before ${operation}.`, | |
), | |
P2014: (operation) => { | |
switch (operation) { | |
case 'create': | |
return new BadRequestException( | |
`Cannot create record because the referenced data does not exist. Ensure related data exists.`, | |
); | |
case 'delete': | |
return new BadRequestException( | |
`Unable to delete record because it is linked to other data. Update or delete dependent records first.`, | |
); | |
default: | |
return new BadRequestException(`Foreign key constraint error.`); | |
} | |
}, | |
P2015: () => | |
new InternalServerErrorException( | |
`A record with the required ID was expected but not found. Please retry.`, | |
), | |
P2016: (operation) => | |
new InternalServerErrorException( | |
`Query ${operation} failed because the record could not be fetched. Ensure the query is correct.`, | |
), | |
P2017: (operation) => | |
new InternalServerErrorException( | |
`Connected records were not found for ${operation}. Check related data.`, | |
), | |
P2018: () => | |
new InternalServerErrorException( | |
`The required connection could not be established. Please check relationships.`, | |
), | |
P2019: (_operation, meta) => | |
new InternalServerErrorException( | |
`Invalid input for ${meta?.details || 'a field'}. Please ensure data conforms to expectations.`, | |
), | |
P2021: (_operation, meta) => | |
new InternalServerErrorException( | |
`The ${meta?.model || 'model'} was not found in the database.`, | |
), | |
P2025: (operation, meta) => | |
new NotFoundException( | |
`The ${meta?.model || 'record'} you are trying to ${operation} does not exist. It may have been deleted.`, | |
), | |
P2031: () => | |
new InternalServerErrorException( | |
`Invalid Prisma Client initialization error. Please check configuration.`, | |
), | |
P2033: (operation) => | |
new InternalServerErrorException( | |
`Insufficient database write permissions for ${operation}.`, | |
), | |
P2034: (operation) => | |
new InternalServerErrorException( | |
`Database read-only transaction failed during ${operation}.`, | |
), | |
P2037: (operation) => | |
new InternalServerErrorException( | |
`Unsupported combinations of input types for ${operation}. Please correct the query or input.`, | |
), | |
P1000: () => | |
new InternalServerErrorException( | |
`Database authentication failed. Verify your credentials and try again.`, | |
), | |
P1001: () => | |
new InternalServerErrorException( | |
`The database server could not be reached. Please check its availability.`, | |
), | |
P1002: () => | |
new InternalServerErrorException( | |
`Connection to the database timed out. Verify network connectivity and server availability.`, | |
), | |
P1015: (operation) => | |
new InternalServerErrorException( | |
`Migration failed. Unable to complete ${operation}. Check migration history or database state.`, | |
), | |
P1017: () => | |
new InternalServerErrorException( | |
`Database connection failed. Ensure the database is online and credentials are correct.`, | |
), | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Prisma } from '@prisma/client'; | |
import { | |
Operations, | |
DelegateArgs, | |
DelegateReturnTypes, | |
DataArgs, | |
WhereArgs, | |
} from './base.type'; | |
import { | |
NotFoundException, | |
InternalServerErrorException, | |
} from '@nestjs/common'; | |
import { ERROR_MAP ,operationT, PrismaErrorCode} from './errorMap.prisma.ts'; | |
/** | |
* 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 { [K in Operations]: (args: unknown) => Promise<unknown> }, | |
A extends DelegateArgs<D>, | |
R extends DelegateReturnTypes<D>, | |
> { | |
/** | |
* Initializes the BaseService with the specified model. | |
* @param model - The Prisma model delegate for database operations. | |
*/ | |
constructor(protected model: D) {} | |
/** | |
* Retrieves the name of the model dynamically. | |
* @returns {string} - The name of the model. | |
*/ | |
private getModelName(): string { | |
const modelName = this.model.constructor.name; | |
return modelName; | |
} | |
/** | |
* 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.model.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.model.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.model.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.model.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']): Promise<R['create']> { | |
try { | |
return this.model.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']): Promise<R['createMany']> { | |
try { | |
return this.model.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']): Promise<R['update']> { | |
try { | |
return this.model.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.model.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.model.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']): Promise<R['delete']> { | |
try { | |
return this.model.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.model.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.model.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.model.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']): Promise<R['deleteMany']> { | |
try { | |
return this.model.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.model.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: '[email protected]' }, create: { email: '[email protected]', name: 'John' } }); | |
*/ | |
async findOrCreate(args: { | |
where: WhereArgs<A['findUnique']>; | |
create: DataArgs<A['create']>; | |
}): Promise<R['findUnique'] | R['create']> { | |
try { | |
const existing = (await this.model.findUnique({ | |
where: args.where, | |
} as any)) as R['findUnique']; | |
if (existing) { | |
return existing; | |
} | |
return this.model.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: '[email protected]' }); | |
*/ | |
async exists(where: WhereArgs<A['findUnique']>): Promise<boolean> { | |
try { | |
const count = (await this.model.count({ where } as any)) as number; | |
return count > 0; | |
} catch (error) { | |
this.handleError(error, 'read'); | |
} | |
} | |
/** | |
* Soft deletes a record by setting `isDeleted` to true. | |
* @param id - The ID of the record to soft delete. | |
* @param data - Additional data to update on soft delete. | |
* @returns {Promise<R['update']>} - A promise resolving to the updated record. | |
* @example | |
* const softDeletedUser = await service.softDeleteById('user_id', { reason: 'User requested deletion' }); | |
*/ | |
async softDeleteById( | |
id: string, | |
data: Partial<DataArgs<A['update']>>, | |
): Promise<R['update']> { | |
try { | |
return this.model.update({ | |
where: { id }, | |
data: { ...data, isDeleted: true } as any, | |
}) as Promise<R['update']>; | |
} catch (error) { | |
this.handleError(error, 'delete'); | |
} | |
} | |
/** | |
* Restores a soft-deleted record by setting `isDeleted` to false. | |
* @param id - The ID of the record to restore. | |
* @param data - Additional data to update on restore. | |
* @returns {Promise<R['update']>} - A promise resolving to the updated record. | |
* @example | |
* const restoredUser = await service.restoreById('user_id', { reason: 'User requested restoration' }); | |
*/ | |
async restoreById( | |
id: string, | |
data: Partial<DataArgs<A['update']>>, | |
): Promise<R['update']> { | |
try { | |
return this.model.update({ | |
where: { id }, | |
data: { ...data, isDeleted: false } 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['findUnique']>; | |
}): Promise<R['findMany']> { | |
const { page, pageSize, where } = args; | |
try { | |
return this.model.findMany({ | |
where, | |
skip: (page - 1) * pageSize, | |
take: pageSize, | |
} as any) as Promise<R['findMany']>; | |
} catch (error) { | |
this.handleError(error, 'read'); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment