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:
@@ -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
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user