Skip to content

Instantly share code, notes, and snippets.

@copy-ninja1
Last active March 22, 2025 16:25
Show Gist options
  • Save copy-ninja1/8a8353916b444e33f34d6ee56a48a9c2 to your computer and use it in GitHub Desktop.
Save copy-ninja1/8a8353916b444e33f34d6ee56a48a9c2 to your computer and use it in GitHub Desktop.
Prisma Base Service with nestjs
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;
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.`,
),
};
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