nestjs-prisma-base
Version:
A comprehensive NestJS package providing base classes, utilities, and decorators for building CRUD APIs with Prisma ORM integration, featuring pagination, search, filtering, relation loading, configurable DTOs, and modular composition capabilities.
318 lines • 13.2 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseService = void 0;
const common_1 = require("@nestjs/common");
const prisma_service_1 = require("../prisma/prisma.service");
const query_builder_1 = require("./query-builder");
const relation_validator_1 = require("./relation-validator");
let BaseService = class BaseService {
constructor(prisma) {
this.prisma = prisma;
this.paginationConfig = {
defaultLimit: 10,
maxLimit: 100,
allowUnlimited: false,
};
this.searchConfig = {
defaultSearchFields: [],
caseSensitive: false,
searchMode: 'contains',
maxSearchFields: 10,
};
this.relationConfig = {
maxDepth: 3,
allowNested: true,
};
}
validateLimit(limit) {
if (limit === 0 || limit === -1) {
if (!this.paginationConfig.allowUnlimited) {
throw new common_1.BadRequestException(`Unlimited results are not allowed. Maximum limit is ${this.paginationConfig.maxLimit}`);
}
return Number.MAX_SAFE_INTEGER;
}
if (limit < 1) {
throw new common_1.BadRequestException('Limit must be a positive number');
}
if (limit > this.paginationConfig.maxLimit) {
throw new common_1.BadRequestException(`Limit ${limit} exceeds maximum allowed limit of ${this.paginationConfig.maxLimit}`);
}
return limit;
}
validatePage(page) {
if (page < 1) {
throw new common_1.BadRequestException('Page must be a positive number starting from 1');
}
return page;
}
validateSearchFields(searchFields) {
if (searchFields.length > this.searchConfig.maxSearchFields) {
throw new common_1.BadRequestException(`Too many search fields. Maximum allowed: ${this.searchConfig.maxSearchFields}`);
}
if (this.searchConfig.defaultSearchFields.length === 0 && searchFields.length > 0) {
throw new common_1.BadRequestException('Search functionality is not configured for this service');
}
return searchFields;
}
buildSearchConditions(options) {
const conditions = {};
if (options.search && options.search.trim()) {
const searchTerm = options.search.trim();
const fieldsToSearch = options.searchFields && options.searchFields.length > 0
? this.validateSearchFields(options.searchFields)
: this.searchConfig.defaultSearchFields;
if (fieldsToSearch.length > 0) {
const searchConditions = fieldsToSearch.map((field) => {
const condition = {};
switch (this.searchConfig.searchMode) {
case 'startsWith':
condition[field] = {
startsWith: searchTerm,
mode: this.searchConfig.caseSensitive ? 'default' : 'insensitive',
};
break;
case 'endsWith':
condition[field] = {
endsWith: searchTerm,
mode: this.searchConfig.caseSensitive ? 'default' : 'insensitive',
};
break;
case 'contains':
default:
condition[field] = {
contains: searchTerm,
mode: this.searchConfig.caseSensitive ? 'default' : 'insensitive',
};
break;
}
return condition;
});
conditions.OR = searchConditions;
}
}
if (options.filters && Object.keys(options.filters).length > 0) {
Object.entries(options.filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
conditions[key] = value;
}
});
}
return Object.keys(conditions).length > 0 ? conditions : undefined;
}
buildOrderConditions(orderBy) {
if (!orderBy || Object.keys(orderBy).length === 0) {
return undefined;
}
return Object.entries(orderBy).map(([field, direction]) => ({
[field]: direction,
}));
}
async findAll(page = 1, limit, options) {
const requestedLimit = limit ?? this.paginationConfig.defaultLimit;
const validatedPage = this.validatePage(page);
const validatedLimit = this.validateLimit(requestedLimit);
const isUnlimited = validatedLimit === Number.MAX_SAFE_INTEGER;
const queryLimit = isUnlimited ? undefined : validatedLimit;
const skip = isUnlimited ? 0 : (validatedPage - 1) * validatedLimit;
const whereConditions = options ? this.buildSearchConditions(options) : undefined;
const orderByConditions = options?.orderBy ? this.buildOrderConditions(options.orderBy) : undefined;
const queryOptions = {
skip: isUnlimited ? undefined : skip,
take: queryLimit,
};
if (whereConditions) {
queryOptions.where = whereConditions;
}
if (orderByConditions) {
queryOptions.orderBy = orderByConditions;
}
const [data, total] = await Promise.all([
this.prisma[this.modelName].findMany(queryOptions),
this.prisma[this.modelName].count({
where: whereConditions,
}),
]);
const actualLimit = isUnlimited ? total : validatedLimit;
const totalPages = isUnlimited ? 1 : Math.ceil(total / validatedLimit);
const hasNext = isUnlimited ? false : validatedPage < totalPages;
const hasPrev = validatedPage > 1;
const meta = {
total,
page: validatedPage,
limit: actualLimit,
totalPages,
hasNext,
hasPrev,
};
return {
data,
meta,
};
}
async findAllSimple(page = 1, limit, options) {
const requestedLimit = limit ?? this.paginationConfig.defaultLimit;
const validatedPage = this.validatePage(page);
const validatedLimit = this.validateLimit(requestedLimit);
const isUnlimited = validatedLimit === Number.MAX_SAFE_INTEGER;
const queryLimit = isUnlimited ? undefined : validatedLimit;
const skip = isUnlimited ? 0 : (validatedPage - 1) * validatedLimit;
const whereConditions = options ? this.buildSearchConditions(options) : undefined;
const orderByConditions = options?.orderBy ? this.buildOrderConditions(options.orderBy) : undefined;
const queryOptions = {
skip: isUnlimited ? undefined : skip,
take: queryLimit,
};
if (whereConditions) {
queryOptions.where = whereConditions;
}
if (orderByConditions) {
queryOptions.orderBy = orderByConditions;
}
return this.prisma[this.modelName].findMany(queryOptions);
}
convertId(id) {
if (typeof id === 'number') {
return id;
}
const numericId = parseInt(id, 10);
if (!isNaN(numericId) && numericId.toString() === id) {
return numericId;
}
return id;
}
async findOne(id) {
const convertedId = this.convertId(id);
const record = await this.prisma[this.modelName].findUnique({
where: { id: convertedId },
});
if (!record) {
throw new common_1.NotFoundException(`${this.modelName} with ID "${id}" not found`);
}
return record;
}
async create(data) {
return this.prisma[this.modelName].create({
data,
});
}
async update(id, data) {
const convertedId = this.convertId(id);
try {
return (await this.prisma[this.modelName].update({
where: { id: convertedId },
data,
}));
}
catch (error) {
if (error?.code === 'P2025') {
throw new common_1.NotFoundException(`${this.modelName} with ID "${id}" not found`);
}
throw error;
}
}
async remove(id) {
const convertedId = this.convertId(id);
try {
return (await this.prisma[this.modelName].delete({
where: { id: convertedId },
}));
}
catch (error) {
if (error?.code === 'P2025') {
throw new common_1.NotFoundException(`${this.modelName} with ID "${id}" not found`);
}
throw error;
}
}
processRelations(options) {
let requestedIncludes;
if (options.requestedIncludes) {
requestedIncludes = options.requestedIncludes;
}
else if (options.include) {
requestedIncludes = options.include;
}
const validationResult = relation_validator_1.RelationValidator.validateIncludes(requestedIncludes, this.relationConfig);
if (validationResult.invalidKeys.length > 0) {
console.warn(`Invalid relation keys ignored: ${validationResult.invalidKeys.join(', ')}`);
}
if (validationResult.depthExceeded) {
console.warn(`Maximum relation depth (${this.relationConfig.maxDepth}) exceeded. Relations were truncated.`);
}
return validationResult.validatedIncludes;
}
buildAdvancedQueryOptions(options) {
const queryResult = query_builder_1.AdvancedQueryBuilder.buildQuery(options, this.searchConfig);
const validatedIncludes = this.processRelations(options);
if (Object.keys(validatedIncludes).length > 0) {
queryResult.include = relation_validator_1.RelationValidator.mergeIncludes(queryResult.include, validatedIncludes);
}
return queryResult;
}
async findAllAdvanced(page = 1, limit, options) {
const requestedLimit = limit ?? this.paginationConfig.defaultLimit;
const validatedPage = this.validatePage(page);
const validatedLimit = this.validateLimit(requestedLimit);
const isUnlimited = validatedLimit === Number.MAX_SAFE_INTEGER;
const queryLimit = isUnlimited ? undefined : validatedLimit;
const skip = isUnlimited ? 0 : (validatedPage - 1) * validatedLimit;
const queryResult = options ? this.buildAdvancedQueryOptions(options) : {};
const queryOptions = {
skip: isUnlimited ? undefined : skip,
take: queryLimit,
};
if (queryResult.where) {
queryOptions.where = queryResult.where;
}
if (queryResult.include) {
queryOptions.include = queryResult.include;
}
if (queryResult.select) {
queryOptions.select = queryResult.select;
}
if (queryResult.orderBy) {
queryOptions.orderBy = queryResult.orderBy;
}
const [data, total] = await Promise.all([
this.prisma[this.modelName].findMany(queryOptions),
this.prisma[this.modelName].count({
where: queryResult.where,
}),
]);
const actualLimit = isUnlimited ? total : validatedLimit;
const totalPages = isUnlimited ? 1 : Math.ceil(total / validatedLimit);
const hasNext = isUnlimited ? false : validatedPage < totalPages;
const hasPrev = validatedPage > 1;
const meta = {
total,
page: validatedPage,
limit: actualLimit,
totalPages,
hasNext,
hasPrev,
};
return {
data,
meta,
};
}
async findAllAdvancedSimple(page = 1, limit, options) {
const result = await this.findAllAdvanced(page, limit, options);
return result.data;
}
};
exports.BaseService = BaseService;
exports.BaseService = BaseService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], BaseService);
//# sourceMappingURL=base.service.js.map