Saltar al contenido principal

Attributes Module

Overview

The Attributes module manages product attribute definitions scoped to a business. An attribute represents a named property that can be attached to products for variant modeling — examples include Color, Size, and Warranty Period.

Attributes are catalog-level records. Each attribute can have multiple attribute values (handled by a separate module, e.g. attribute-values), and those values are combined to form product variants.


Architecture

This module follows Hexagonal Architecture with strict inward dependencies:

interfaces (controller, DTOs, query)
└── application (service / use cases)
└── domain (IAttributesRepository port, sortableAttributeKeys)
└── infrastructure (AttributesRepository — Kysely adapter)

Layer Responsibilities

LayerFileResponsibility
Domaindomain/attributes-repository.domain.tsRepository interface (port) + sortableAttributeKeys constant
Applicationapplication/attributes.service.tsOrchestrates use cases, delegates to repository port
Infrastructureinfrastructure/attributes.repository.tsKysely queries, implements IAttributesRepository
Interfaceinterfaces/attributes.controller.tsHTTP routing, request mapping, RBAC guards
Interfaceinterfaces/dtos/Request body validation (class-validator + Swagger)
Interfaceinterfaces/query/Pagination, sorting, and filter query params

Dependency rule: sortableAttributeKeys is defined in the domain layer and imported by both the query and repository layers. Never import interface-layer symbols into the domain.


Domain Concepts

  • Attribute — a named, business-scoped property definition. Fields: id, name, description, businessId, isActive, createdBy, updatedBy, createdAt, updatedAt.
  • Sortable keys — defined in the domain as ["name", "description"].

Access Control

  • AuthGuard (global) — validates Firebase ID token on every request.
  • RolesGuard — enforces role-based access via @UseGuards(RolesGuard).
  • PermissionResource — maps to PolicyResource.Attribute for CASL ability checks.
  • Multi-tenancy — all queries are scoped by businessId to prevent cross-tenant data access.

Use Cases

Use CaseMethodDescription
Create attributecreateAttributeInserts a new attribute for a business
List attributesgetAllAttributesReturns paginated list with search + filter (scoped by businessId)
Get attributegetAttributeByIdFetches a single attribute scoped by id + businessId; 404 if missing
Update attributeupdateAttributePartial update via PATCH, scoped by businessId
Delete attributedeleteAttributeHard deletes the attribute, scoped by id + businessId

API Endpoints

Base path: /attributes

POST /attributes

Creates a new attribute.

Request body:

{
"name": "Warranty Period",
"description": "Duration of the product warranty in months.",
"businessId": "550e8400-e29b-41d4-a716-446655440001",
"isActive": true,
"createdBy": "550e8400-e29b-41d4-a716-446655440000"
}

Responses: 201 Created | 400 Validation error | 401 Unauthorized | 403 Forbidden


GET /attributes

Returns a paginated list of attributes for a business.

Query params:

ParamRequiredDescription
businessIdYesBusiness UUID to scope attributes
searchNoCase-insensitive search on name and description
pageNoPage number (default: 1)
sizeNoPage size (default: 10; 0 = no limit)
orderByNoname or description
orderNoasc or desc

Responses: 200 OK | 401 Unauthorized | 403 Forbidden


GET /attributes/:id

Returns a single attribute by UUID, scoped by business.

Query params:

ParamRequiredDescription
businessIdYesBusiness UUID to scope the lookup

Responses: 200 OK | 401 Unauthorized | 403 Forbidden | 404 Not Found


PATCH /attributes/:id

Partially updates an attribute. Only provided fields are changed.

Request body (all optional except updatedBy and businessId):

{
"description": "Duration of the product warranty in months.",
"businessId": "550e8400-e29b-41d4-a716-446655440001",
"updatedBy": "550e8400-e29b-41d4-a716-446655440000"
}

Responses: 200 OK | 400 Validation error | 401 Unauthorized | 403 Forbidden | 404 Not Found


DELETE /attributes/:id

Hard deletes an attribute by UUID, scoped by business.

Query params:

ParamRequiredDescription
businessIdYesBusiness UUID to scope the deletion

Responses: 200 OK | 401 Unauthorized | 403 Forbidden | 404 Not Found


Bruno API Collection

Requests are located in:

api-client/flowpos/collections/attributes/
├── attributes.yml # GET /attributes (list)
├── attribute.yml # POST /attributes (create)
├── attribute by Id.yml # GET /attributes/:id
├── attribute_1.yml # PATCH /attributes/:id (update)
└── attribute_2.yml # DELETE /attributes/:id

All requests use {{BASE_URL}}, {{ID_TOKEN}}, {{businessId}}, {{userId}}, and {{attributeId}} environment variables.


Design Decisions

  • description is optional — allows lightweight attribute definitions where a description is not always meaningful.
  • sortableAttributeKeys lives in the domain — prevents the domain layer from importing interface-layer symbols. Both the query class and the repository import it from the domain.
  • Hard delete — attributes are deleted with DELETE. If soft-delete is needed in the future, add an isDeleted column and filter it in findAll.
  • NOT_FOUND (404) on missing attributegetAttributeById raises 404, not 400, to correctly signal a missing resource.
  • businessId required on all operations — enforces multi-tenancy at the API level. GET by ID, PATCH, and DELETE all require businessId to prevent cross-tenant access.
  • Update DTO uses OmitType + PartialType — inherits create fields as optional via PartialType, excludes businessId from inheritance and re-declares it as required, matching the categories module pattern.
  • RolesGuard + PermissionResource — RBAC is enforced at the controller level using PolicyResource.Attribute, consistent with other catalog modules.