Addon Tags Module
Overview
The addon-tags module manages the many-to-many association between addon and tag entities in FlowPOS. Each addonTag record links one addon to one tag and carries an isActive flag for soft-enabling or disabling the association.
Architecture
This module follows the Hexagonal Architecture (ports & adapters) pattern used across the backend:
addon-tags/
├── addon-tags.module.ts # NestJS module wiring
├── application/
│ └── addon-tags.service.ts # Use cases
├── domain/
│ └── addon-tags-repository.domain.ts # IAddonTagsRepository port + ADDON_TAGS_REPOSITORY token + sortableAddonTagKeys
├── infrastructure/
│ └── addon-tags.repository.ts # Kysely DB adapter
└── interfaces/
├── addon-tags.controller.ts # REST endpoints
├── dtos/
│ ├── create-addon-tag.dto.ts
│ └── update-addon-tag.dto.ts
└── query/
└── paginate-addon-tags.query.ts
Dependency flow
interfaces → application → domain ← infrastructure
- Domain owns the
IAddonTagsRepositoryport, theADDON_TAGS_REPOSITORYinjection token, andsortableAddonTagKeys. No framework imports. - Application (service) depends only on the domain interface via
@Inject(ADDON_TAGS_REPOSITORY). - Infrastructure (repository) implements the domain port via Kysely queries.
- Interfaces (controller + DTOs) handles HTTP concerns and delegates to the service.
Dependency Injection
The module uses Symbol-based token DI (consistent with all other modules):
// domain/addon-tags-repository.domain.ts
export const ADDON_TAGS_REPOSITORY = Symbol("ADDON_TAGS_REPOSITORY");
export interface IAddonTagsRepository { ... }
// application/addon-tags.service.ts
@Inject(ADDON_TAGS_REPOSITORY) private readonly addonTagsRepository: IAddonTagsRepository
// addon-tags.module.ts
{ provide: ADDON_TAGS_REPOSITORY, useClass: AddonTagsRepository }
Domain Concepts
| Concept | Description |
|---|---|
addonTag | A join record linking an addon to a tag |
addonId | FK to the addon table |
tagId | FK to the tag table |
isActive | Soft-enables or disables the association |
Main Use Cases
| Use Case | Method | Endpoint |
|---|---|---|
| Create addon tag | POST | /addon-tags |
| List all addon tags | GET | /addon-tags |
| Get addon tag by ID | GET | /addon-tags/:id |
| Update addon tag | PATCH | /addon-tags/:id |
| Delete addon tag | DELETE | /addon-tags/:id |
API Endpoints
All endpoints require a valid Firebase ID token (Authorization: Bearer <token> or flowpos-id-token cookie) and the AddonTag RBAC permission enforced via RolesGuard + @PermissionResource(PolicyResource.AddonTag).
POST /addon-tags
Creates a new addon-tag association.
Required fields: addonId, tagId, isActive, createdBy
{
"addonId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"tagId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isActive": true,
"createdBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
Returns 201 Created with the created record.
GET /addon-tags
Returns a paginated list of all addon-tag associations.
Query parameters:
| Param | Type | Description |
|---|---|---|
page | number | Page number (default: 1) |
size | number | Page size (0 = no limit) |
Note:
orderByandsearchparameters are accepted by the query class but have no effect —addonTaghas no text-searchable columns and the sortable keys set is currently empty.
GET /addon-tags/:id
Returns a single addon tag by UUID. Returns 404 Not Found if the record does not exist.
PATCH /addon-tags/:id
Updates an existing addon-tag record. All fields except updatedBy are optional.
Required fields: updatedBy
{
"isActive": false,
"updatedBy": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
DELETE /addon-tags/:id
Deletes an addon-tag record. Returns 204 No Content on success.
Design Decisions
Symbol-based DI token
The ADDON_TAGS_REPOSITORY Symbol is defined in the domain layer and used by both the application service (consumer) and the module (provider). This follows the project-wide convention for proper dependency inversion — the application layer never imports from infrastructure.
sortableAddonTagKeys lives in the domain layer
The sort-key tuple is defined in domain/addon-tags-repository.domain.ts and re-exported from the query class. When a sortable column is added to the schema, add it to the tuple in the domain file.
No free-text search
addonTag has no text-searchable columns. The search parameter is accepted to maintain API compatibility with the common query mixin but has no effect on the query.
Soft activation vs. deletion
The isActive flag allows disabling a tag association without permanently removing the record. Use PATCH to toggle it; use DELETE only when the record must be permanently removed.
RBAC enforcement
The controller applies @UseGuards(RolesGuard) and @PermissionResource(PolicyResource.AddonTag) at the class level, with @PermissionAction on each route method. This ensures all endpoints are protected by the CASL-based permission system.