Skip to main content

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 IAddonTagsRepository port, the ADDON_TAGS_REPOSITORY injection token, and sortableAddonTagKeys. 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

ConceptDescription
addonTagA join record linking an addon to a tag
addonIdFK to the addon table
tagIdFK to the tag table
isActiveSoft-enables or disables the association

Main Use Cases

Use CaseMethodEndpoint
Create addon tagPOST/addon-tags
List all addon tagsGET/addon-tags
Get addon tag by IDGET/addon-tags/:id
Update addon tagPATCH/addon-tags/:id
Delete addon tagDELETE/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:

ParamTypeDescription
pagenumberPage number (default: 1)
sizenumberPage size (0 = no limit)

Note: orderBy and search parameters are accepted by the query class but have no effect — addonTag has 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.

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.