Models Module
Product models represent specific product lines within a brand (e.g., "Air Max 90" under "Nike"). A model belongs to a single brand and a single business.
Domain Concepts
| Entity | Description |
|---|---|
| Model | A named product line linked to a brand and business. Has an optional modelCode for internal referencing. |
| Brand | Parent entity — each model belongs to exactly one brand. |
Architecture
Follows hexagonal architecture:
domain/
models-repository.domain.ts # IModelsRepository port + MODELS_REPOSITORY token
application/
models.service.ts # Use cases (CRUD + paginated list)
infrastructure/
models.repository.ts # Kysely adapter implementing IModelsRepository
interfaces/
models.controller.ts # REST controller with RBAC guards
dtos/
create-model.dto.ts # Validation + Swagger for creation
update-model.dto.ts # Partial update DTO
query/
paginate-models.query.ts # Pagination, search, sort query params
API Endpoints
All endpoints require authentication (Bearer token) and RBAC permission on PolicyResource.Model.
| Method | Path | Permission | Description |
|---|---|---|---|
POST | /models | Create | Create a new model |
GET | /models | Read | List models (paginated, searchable) |
GET | /models/:id | Read | Get a model by ID |
PATCH | /models/:id | Update | Partially update a model |
DELETE | /models/:id | Delete | Delete a model |
Query Parameters (GET /models)
| Param | Type | Required | Description |
|---|---|---|---|
businessId | UUID | No | Filter by business |
search | string | No | Search by model name or brand name |
size | number | No | Page size |
page | number | No | Page number |
orderBy | string | No | Sort field (name) |
order | asc / desc | No | Sort direction |
Create Model (POST /models)
{
"name": "Air Max 90",
"businessId": "uuid",
"brandId": "uuid",
"createdBy": "uuid",
"isActive": true,
"modelCode": "AM90-2026"
}
Update Model (PATCH /models/:id)
{
"name": "Air Max 95",
"updatedBy": "uuid",
"modelCode": "AM95-2026"
}
Design Decisions
- Brand join on list: The
findAllquery joins withbrandto includebrandNamein the response, avoiding N+1 queries. - Filter whitelist: Only whitelisted filter keys are accepted in the repository to prevent SQL injection through dynamic column names.
- No soft delete: Models are hard-deleted. Use
isActive: falseto deactivate instead of deleting.