Skip to main content

Forms Module

Overview

The forms module manages system-level navigation entries for the FlowPOS PWA. Each "form" represents a route/page with a display name, icon, and optional module association. Forms are global (not scoped to a business) and are used to build dynamic navigation menus.

Architecture

forms/
├── forms.module.ts # Module definition
├── application/
│ └── forms.service.ts # CRUD orchestration
├── domain/
│ └── forms-repository.domain.ts # Repository port (interface) + DI token
├── infrastructure/
│ └── forms.repository.ts # Kysely DB adapter
└── interfaces/
├── forms.controller.ts # HTTP endpoints
├── dtos/
│ ├── create-form.dto.ts
│ └── update-form.dto.ts
└── query/
└── paginate-forms.query.ts

Layer Responsibilities

  • Domain: Repository interface (IFormsRepository), FORMS_REPOSITORY injection token, and sortable key constants. Framework-agnostic.
  • Application: FormsService orchestrates CRUD operations and pagination. Depends only on the domain port via DI token.
  • Infrastructure: FormsRepository implements the domain port using Kysely queries against PostgreSQL.
  • Interfaces: FormsController maps HTTP routes to service methods. DTOs validate request payloads with class-validator and document schemas via Swagger decorators.

Domain Concepts

Form

A form represents a navigation entry in the PWA menu system:

FieldTypeDescription
idUUIDPrimary key
pathstringRoute path (e.g., /inventory/InventoryDashboardPage) — unique
namestringi18n translation key (e.g., menu.inventoryDashboard)
iconstringIcon name for the navigation menu (nullable)
moduleIdUUIDFK to module table — groups forms by feature module
isActivebooleanControls visibility in navigation
createdBy / updatedByUUIDFK to user — audit trail

Module Association

Forms link to modules (e.g., restaurant, production, data-import) to organize the PWA navigation by feature area. The module table defines top-level feature groups; forms define the pages within each group.

API Endpoints

MethodPathDescription
POST/formsCreate a new form
GET/formsList forms (paginated, searchable, sortable)
GET/forms/:idGet a form by ID
PATCH/forms/:idPartially update a form
DELETE/forms/:idDelete a form

Query Parameters (GET /forms)

ParameterTypeRequiredDescription
searchstringNoSearch by path, name, or icon (case-insensitive)
pagenumberNoPage number (1-based)
sizenumberNoPage size
orderBystringNoSort field: path, name, icon
orderstringNoSort direction: asc, desc

Example: Create Form

POST /forms
{
"path": "/billing/BillingReportsPage",
"name": "menu.billingReports",
"icon": "article",
"moduleId": "550e8400-e29b-41d4-a716-446655440000",
"isActive": true,
"createdBy": "550e8400-e29b-41d4-a716-446655440001"
}

Example: Update Form

PATCH /forms/:id
{
"name": "menu.updatedName",
"icon": "settings",
"updatedBy": "550e8400-e29b-41d4-a716-446655440001"
}

Design Decisions

  1. Global scope: Forms are not business-scoped. They represent system-wide navigation entries that all tenants share. Per-business visibility is controlled at the module assignment level, not at the form level.

  2. Token-based DI: The repository is injected via FORMS_REPOSITORY symbol token, keeping the application layer decoupled from the infrastructure implementation.

  3. Unique path constraint: The path column has a unique index, preventing duplicate navigation entries. Migrations use onConflict(path).doNothing() for idempotent seeding.

  4. Seeded via migrations: Standard forms (restaurant pages, production pages, import pages) are inserted by database migrations, not by application code. New modules seed their forms in their own migration files.