feat: implement inventory, product brand, product category, product info, product, role, store, user, and vendor modules with CRUD operations

- Added Create and Update DTOs for Inventory, Product Brand, Product Category, Product Info, Product, Role, Store, User, and Vendor.
- Implemented InventoriesController, ProductBrandsController, ProductCategoriesController, ProductInfoController, ProductsController, RolesController, StoresController, UsersController, and VendorsController with respective CRUD endpoints.
- Developed InventoriesService, ProductBrandsService, ProductCategoriesService, ProductInfoService, ProductsService, RolesService, StoresService, UsersService, and VendorsService to handle business logic.
- Created PrismaModule and PrismaService for database interactions using Prisma with MariaDB adapter.
- Configured application with Swagger for API documentation and added global interceptors for logging and response mapping.
- Established e2e testing setup with Jest for the application.
This commit is contained in:
2025-12-04 21:05:57 +03:30
commit 621e15dd02
98 changed files with 26046 additions and 0 deletions
@@ -0,0 +1,32 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common'
import { Observable, tap } from 'rxjs'
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest()
const method = req?.method ?? 'UNKNOWN'
const url = req?.url ?? 'UNKNOWN'
const now = Date.now()
console.log(`[Request] ${method} ${url}`)
return next.handle().pipe(
tap({
next: () => {
const ms = Date.now() - now
console.log(`[Response] ${method} ${url} - ${ms}ms`)
},
error: err => {
const ms = Date.now() - now
console.error(`[Error] ${method} ${url} - ${ms}ms`, err?.message ?? err)
},
}),
)
}
}
@@ -0,0 +1,126 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
/**
* ResponseMappingInterceptor
*
* Wraps successful handler results in a consistent response envelope.
* Example output:
* {
* statusCode: 200,
* timestamp: '2025-12-04T19:00:00.000Z',
* path: '/api/v1/users',
* data: { ... }
* }
*/
@Injectable()
export class ResponseMappingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const http = context.switchToHttp()
const response = http.getResponse()
const request = http.getRequest()
return next.handle().pipe(
map(data => {
const statusCode = response?.statusCode ?? 200
// Normalize list/paginated responses into { data, meta }
let payload = data
let meta: any = undefined
// Case: plain array -> treat as list
if (Array.isArray(data)) {
payload = data
meta = {
totalRecords: data.length,
totalPages: 1,
page: 1,
perPage: data.length,
}
} else if (data && typeof data === 'object') {
// Common pagination shapes
// 1) { items: [...], total: N, page, perPage }
if (
Array.isArray((data as any).items) &&
typeof (data as any).total === 'number'
) {
const items = (data as any).items
const total = (data as any).total
const perPage = (data as any).perPage ?? (data as any).limit ?? items.length
const page = (data as any).page ?? 1
payload = items
meta = {
totalRecords: total,
totalPages: Math.max(1, Math.ceil(total / perPage)),
page,
perPage,
}
// 2) Sequelize-like: { rows: [...], count: N }
} else if (
Array.isArray((data as any).rows) &&
typeof (data as any).count === 'number'
) {
const items = (data as any).rows
const total = (data as any).count
const perPage = (data as any).limit ?? (data as any).perPage ?? items.length
const page = (data as any).page ?? 1
payload = items
meta = {
totalRecords: total,
totalPages: Math.max(1, Math.ceil(total / perPage)),
page,
perPage,
}
// 3) { data: [...], total, page, perPage }
} else if (
Array.isArray((data as any).data) &&
typeof (data as any).total === 'number'
) {
const items = (data as any).data
const total = (data as any).total
const perPage = (data as any).perPage ?? (data as any).limit ?? items.length
const page = (data as any).page ?? 1
payload = items
meta = {
totalRecords: total,
totalPages: Math.max(1, Math.ceil(total / perPage)),
page,
perPage,
}
// 4) fallback: object contains items array and optional total
} else if (Array.isArray((data as any).items)) {
const items = (data as any).items
const total =
typeof (data as any).total === 'number' ? (data as any).total : items.length
const perPage = (data as any).perPage ?? (data as any).limit ?? items.length
const page = (data as any).page ?? 1
payload = items
meta = {
totalRecords: total,
totalPages: Math.max(1, Math.ceil(total / perPage)),
page,
perPage,
}
}
}
const envelope: any = {
statusCode,
timestamp: new Date().toISOString(),
path: request?.url ?? '',
data: payload,
}
if (meta) envelope.meta = meta
return envelope
}),
)
}
}