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_REPOSITORYinjection token, and sortable key constants. Framework-agnostic. - Application:
FormsServiceorchestrates CRUD operations and pagination. Depends only on the domain port via DI token. - Infrastructure:
FormsRepositoryimplements the domain port using Kysely queries against PostgreSQL. - Interfaces:
FormsControllermaps 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:
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key |
path | string | Route path (e.g., /inventory/InventoryDashboardPage) — unique |
name | string | i18n translation key (e.g., menu.inventoryDashboard) |
icon | string | Icon name for the navigation menu (nullable) |
moduleId | UUID | FK to module table — groups forms by feature module |
isActive | boolean | Controls visibility in navigation |
createdBy / updatedBy | UUID | FK 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
| Method | Path | Description |
|---|---|---|
POST | /forms | Create a new form |
GET | /forms | List forms (paginated, searchable, sortable) |
GET | /forms/:id | Get a form by ID |
PATCH | /forms/:id | Partially update a form |
DELETE | /forms/:id | Delete a form |
Query Parameters (GET /forms)
| Parameter | Type | Required | Description |
|---|---|---|---|
search | string | No | Search by path, name, or icon (case-insensitive) |
page | number | No | Page number (1-based) |
size | number | No | Page size |
orderBy | string | No | Sort field: path, name, icon |
order | string | No | Sort 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
-
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.
-
Token-based DI: The repository is injected via
FORMS_REPOSITORYsymbol token, keeping the application layer decoupled from the infrastructure implementation. -
Unique path constraint: The
pathcolumn has a unique index, preventing duplicate navigation entries. Migrations useonConflict(path).doNothing()for idempotent seeding. -
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.