@flusys/nestjs-form-builder 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,138 +1,85 @@
1
- # Form Builder Package Guide
1
+ # @flusys/nestjs-form-builder
2
2
 
3
- > **Package:** `@flusys/nestjs-form-builder`
4
- > **Version:** 4.0.1
5
- > **Type:** Dynamic form management with schema versioning and access control
3
+ > Dynamic form management for NestJS — JSON schema definitions, schema versioning, access control (PUBLIC/AUTHENTICATED/ACTION_GROUP), draft submissions, and a server-side computed fields engine.
6
4
 
7
- This guide covers the NestJS form builder package - dynamic form creation, submission storage, and multi-tenant support.
5
+ [![npm version](https://img.shields.io/npm/v/@flusys/nestjs-form-builder.svg)](https://www.npmjs.com/package/@flusys/nestjs-form-builder)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![NestJS](https://img.shields.io/badge/NestJS-11.x-red.svg)](https://nestjs.com/)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)
9
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18.x-green.svg)](https://nodejs.org/)
10
+
11
+ ---
8
12
 
9
13
  ## Table of Contents
10
14
 
11
15
  - [Overview](#overview)
16
+ - [Features](#features)
17
+ - [Compatibility](#compatibility)
12
18
  - [Installation](#installation)
13
- - [Constants](#constants)
14
- - [Package Architecture](#package-architecture)
15
- - [Module Setup](#module-setup)
19
+ - [Quick Start](#quick-start)
20
+ - [Module Registration](#module-registration)
21
+ - [forRoot (Sync)](#forroot-sync)
22
+ - [forRootAsync (Factory)](#forrootasync-factory)
23
+ - [Configuration Reference](#configuration-reference)
24
+ - [Feature Toggles](#feature-toggles)
25
+ - [API Endpoints](#api-endpoints)
16
26
  - [Entities](#entities)
17
- - [DTOs](#dtos)
18
- - [Services](#services)
19
- - [Controllers](#controllers)
20
27
  - [Access Control](#access-control)
21
- - [Multi-Tenant Support](#multi-tenant-support)
22
- - [API Reference](#api-reference)
23
- - [Computed Fields](#computed-fields)
24
- - [Response Mode](#response-mode)
25
- - [Best Practices](#best-practices)
26
- - [Swagger Configuration](#swagger-configuration)
27
- - [Permission Utilities](#permission-utilities)
28
- - [Controller Security](#controller-security)
28
+ - [Schema Versioning](#schema-versioning)
29
+ - [Draft Support](#draft-support)
30
+ - [Computed Fields Engine](#computed-fields-engine)
31
+ - [Exported Services](#exported-services)
32
+ - [Programmatic Usage](#programmatic-usage)
33
+ - [Troubleshooting](#troubleshooting)
34
+ - [License](#license)
29
35
 
30
36
  ---
31
37
 
32
38
  ## Overview
33
39
 
34
- `@flusys/nestjs-form-builder` provides a comprehensive form management system:
35
-
36
- - **Dynamic Forms** - JSON schema-based form definitions
37
- - **Schema Versioning** - Auto-increment version on schema changes
38
- - **Result Snapshots** - Store schema at submission time for historical accuracy
39
- - **Access Control** - Public, authenticated, and permission-based access
40
- - **Multi-Tenant Support** - Optional company isolation
41
- - **POST-only RPC** - Follows project API conventions
42
-
43
- ### Package Hierarchy
44
-
45
- ```
46
- @flusys/nestjs-core ← Foundation
47
-
48
- @flusys/nestjs-shared ← Shared utilities
49
-
50
- @flusys/nestjs-form-builder ← Form management (THIS PACKAGE)
51
- ```
40
+ `@flusys/nestjs-form-builder` provides a complete dynamic form system where form structures are stored as JSON in the database. New forms can be created without any code changes. The module handles schema versioning (submissions snapshot the schema they were submitted against), per-form access control, draft submissions, and server-side computed field evaluation.
52
41
 
53
42
  ---
54
43
 
55
- ## Installation
44
+ ## Features
56
45
 
57
- ```bash
58
- npm install @flusys/nestjs-form-builder @flusys/nestjs-shared @flusys/nestjs-core
59
- ```
46
+ - **Database-driven forms** — JSON schema stored in PostgreSQL; no code changes for new forms
47
+ - **Schema versioning** — Schema version auto-increments on change; each submission captures the version it used
48
+ - **Access types** — `PUBLIC` (no auth), `AUTHENTICATED` (any logged-in user), `ACTION_GROUP` (IAM permission-based)
49
+ - **Draft submissions** — Users can save partial form data and finalize later
50
+ - **Computed fields engine** — Server-side arithmetic computed from submission data using rules and conditions
51
+ - **Slug-based lookup** — Forms accessible by a URL-friendly slug
52
+ - **Company scoping** — Optional `companyId` isolation via `FormWithCompany` entity
53
+ - **Multi-tenant** — Per-tenant DataSource isolation
60
54
 
61
55
  ---
62
56
 
63
- ## Constants
57
+ ## Compatibility
64
58
 
65
- ```typescript
66
- // Injection Token
67
- export const FORM_BUILDER_MODULE_OPTIONS = 'FORM_BUILDER_MODULE_OPTIONS';
68
- ```
59
+ | Package | Version |
60
+ |---------|---------|
61
+ | `@flusys/nestjs-core` | `^4.0.0` |
62
+ | `@flusys/nestjs-shared` | `^4.0.0` |
63
+ | `@nestjs/core` | `^11.0.0` |
64
+ | `typeorm` | `^0.3.0` |
65
+ | Node.js | `>= 18.x` |
69
66
 
70
67
  ---
71
68
 
72
- ## Package Architecture
69
+ ## Installation
73
70
 
74
- ```
75
- nestjs-form-builder/
76
- ├── src/
77
- │ ├── modules/
78
- │ │ └── form-builder.module.ts # Main module
79
- │ │
80
- │ ├── config/
81
- │ │ ├── form-builder.constants.ts # Constants
82
- │ │ └── index.ts
83
- │ │
84
- │ ├── entities/
85
- │ │ ├── form.entity.ts # Main form entity
86
- │ │ ├── form-with-company.entity.ts # Extends Form with company
87
- │ │ ├── form-result.entity.ts # Submissions
88
- │ │ └── index.ts
89
- │ │
90
- │ ├── dtos/
91
- │ │ ├── form.dto.ts # Form DTOs
92
- │ │ ├── form-result.dto.ts # Result DTOs
93
- │ │ └── index.ts
94
- │ │
95
- │ ├── services/
96
- │ │ ├── form-builder-config.service.ts # Config service
97
- │ │ ├── form-builder-datasource.provider.ts
98
- │ │ ├── form.service.ts # Form CRUD
99
- │ │ ├── form-result.service.ts # Submission handling
100
- │ │ └── index.ts
101
- │ │
102
- │ ├── controllers/
103
- │ │ ├── form.controller.ts # Form endpoints
104
- │ │ ├── form-result.controller.ts # Result endpoints
105
- │ │ └── index.ts
106
- │ │
107
- │ ├── enums/
108
- │ │ ├── form-access-type.enum.ts
109
- │ │ └── index.ts
110
- │ │
111
- │ ├── interfaces/
112
- │ │ ├── form.interface.ts
113
- │ │ ├── form-result.interface.ts
114
- │ │ ├── form-builder-module.interface.ts
115
- │ │ └── index.ts
116
- │ │
117
- │ ├── utils/
118
- │ │ ├── permission.utils.ts # Permission validation
119
- │ │ ├── computed-field.utils.ts # Computed field calculation
120
- │ │ └── index.ts
121
- │ │
122
- │ ├── docs/
123
- │ │ ├── form-builder-swagger.config.ts
124
- │ │ └── index.ts
125
- │ │
126
- │ └── index.ts # Public API
71
+ ```bash
72
+ npm install @flusys/nestjs-form-builder @flusys/nestjs-shared @flusys/nestjs-core
127
73
  ```
128
74
 
129
75
  ---
130
76
 
131
- ## Module Setup
77
+ ## Quick Start
132
78
 
133
- ### Basic Setup (Single Tenant)
79
+ ### Minimal Setup (Single Database)
134
80
 
135
81
  ```typescript
82
+ import { Module } from '@nestjs/common';
136
83
  import { FormBuilderModule } from '@flusys/nestjs-form-builder';
137
84
 
138
85
  @Module({
@@ -146,11 +93,12 @@ import { FormBuilderModule } from '@flusys/nestjs-form-builder';
146
93
  },
147
94
  config: {
148
95
  defaultDatabaseConfig: {
149
- host: 'localhost',
150
- port: 5432,
151
- username: 'postgres',
152
- password: 'password',
153
- database: 'flusys',
96
+ type: 'postgres',
97
+ host: process.env.DB_HOST,
98
+ port: Number(process.env.DB_PORT ?? 5432),
99
+ username: process.env.DB_USER,
100
+ password: process.env.DB_PASSWORD,
101
+ database: process.env.DB_NAME,
154
102
  },
155
103
  },
156
104
  }),
@@ -159,906 +107,344 @@ import { FormBuilderModule } from '@flusys/nestjs-form-builder';
159
107
  export class AppModule {}
160
108
  ```
161
109
 
162
- ### With Company Feature
110
+ After startup, create forms via `POST /form-builder/form/insert` and collect submissions via `POST /form-builder/result/insert`.
111
+
112
+ ---
113
+
114
+ ## Module Registration
115
+
116
+ ### forRoot (Sync)
163
117
 
164
118
  ```typescript
165
119
  FormBuilderModule.forRoot({
166
- bootstrapAppConfig: {
167
- databaseMode: 'single',
168
- enableCompanyFeature: true, // Enable company isolation
169
- },
170
- config: {
171
- defaultDatabaseConfig: dbConfig,
172
- },
120
+ global?: boolean;
121
+ includeController?: boolean; // Default: true
122
+ bootstrapAppConfig?: {
123
+ databaseMode: 'single' | 'multi-tenant';
124
+ enableCompanyFeature: boolean; // true = FormWithCompany entity
125
+ };
126
+ config?: IFormBuilderConfig;
173
127
  })
174
128
  ```
175
129
 
176
- ### Async Configuration
130
+ ### forRootAsync (Factory)
177
131
 
178
132
  ```typescript
133
+ import { ConfigService } from '@nestjs/config';
134
+
179
135
  FormBuilderModule.forRootAsync({
136
+ global: true,
137
+ includeController: true,
180
138
  bootstrapAppConfig: {
181
139
  databaseMode: 'single',
182
140
  enableCompanyFeature: true,
183
141
  },
184
- useFactory: async (configService: ConfigService) => ({
185
- defaultDatabaseConfig: configService.getDatabaseConfig(),
142
+ imports: [ConfigModule],
143
+ useFactory: (configService: ConfigService) => ({
144
+ defaultDatabaseConfig: {
145
+ type: 'postgres',
146
+ host: configService.get('DB_HOST'),
147
+ port: configService.get<number>('DB_PORT'),
148
+ username: configService.get('DB_USER'),
149
+ password: configService.get('DB_PASSWORD'),
150
+ database: configService.get('DB_NAME'),
151
+ },
186
152
  }),
187
153
  inject: [ConfigService],
188
154
  })
189
155
  ```
190
156
 
191
- ### Migration Configuration
192
-
193
- Add form builder entities to your migration config:
194
-
195
- ```typescript
196
- import { getFormBuilderEntitiesByConfig } from '@flusys/nestjs-form-builder/entities';
197
-
198
- function getEntitiesForTenant(tenantConfig?: ITenantDatabaseConfig): any[] {
199
- const enableCompany = tenantConfig?.enableCompanyFeature ?? false;
200
-
201
- // ... other entities
202
- const formBuilderEntities = getFormBuilderEntitiesByConfig(enableCompany);
203
-
204
- return [...otherEntities, ...formBuilderEntities];
205
- }
206
- ```
207
-
208
- ---
209
-
210
- ## Entities
211
-
212
- ### Entity Groups
213
-
214
- ```typescript
215
- // Core entities (no company feature)
216
- export const FormCoreEntities = [Form, FormResult];
217
-
218
- // Company-specific entities
219
- export const FormCompanyEntities = [FormWithCompany, FormResult];
220
-
221
- // Helper function
222
- export function getFormBuilderEntitiesByConfig(enableCompanyFeature: boolean): any[] {
223
- return enableCompanyFeature ? FormCompanyEntities : FormCoreEntities;
224
- }
225
-
226
- // Base type alias for backwards compatibility
227
- export { Form as FormBase } from './form.entity';
228
- ```
229
-
230
- ### Form
231
-
232
- Main form entity with all form fields:
233
-
234
- | Column | Type | Default | Description |
235
- |--------|------|---------|-------------|
236
- | `name` | `varchar(255)` | required | Form name |
237
- | `description` | `varchar(500)` | null | Optional description |
238
- | `slug` | `varchar(255)` | null | URL-friendly identifier |
239
- | `schema` | `json` | required | Form schema (sections, fields, settings) |
240
- | `schemaVersion` | `int` | 1 | Auto-incremented on schema changes |
241
- | `accessType` | `varchar(50)` | 'AUTHENTICATED' | `public`, `authenticated`, `action_group` |
242
- | `actionGroups` | `simple-array` | null | Permission codes for action_group access |
243
- | `isActive` | `boolean` | true | Form availability |
244
- | `metadata` | `simple-json` | null | Additional data |
245
-
246
- **Indexes (Form):**
247
- - `slug` - Unique index
248
- - `isActive` - Index for filtering active forms
249
-
250
- ### Form vs FormWithCompany
251
-
252
- - **Form** - Used when `enableCompanyFeature: false`
253
- - **FormWithCompany** - Extends Form, adds `companyId` column for tenant isolation
254
-
255
- **FormWithCompany Additional Column:**
256
-
257
- | Column | Type | Description |
258
- |--------|------|-------------|
259
- | `companyId` | `uuid` | Company ID for tenant isolation |
260
-
261
- **FormWithCompany Indexes:**
262
- - `companyId` - Index for company filtering
263
- - `companyId, slug` - Unique compound index (slugs unique per company)
264
- - `companyId, isActive` - Compound index for active forms per company
265
-
266
- ### FormResult
267
-
268
- Stores form submissions:
269
-
270
- | Column | Type | Default | Description |
271
- |--------|------|---------|-------------|
272
- | `formId` | `uuid` | required | Reference to form |
273
- | `schemaVersionSnapshot` | `json` | required | Full schema copy at submission time |
274
- | `schemaVersion` | `int` | required | Schema version at submission |
275
- | `data` | `json` | required | Submitted field values |
276
- | `submittedById` | `uuid` | null | User who submitted (null for public) |
277
- | `submittedAt` | `timestamp` | required | Submission timestamp |
278
- | `isDraft` | `boolean` | false | Draft vs final submission |
279
- | `metadata` | `simple-json` | null | Additional data |
280
-
281
- **Indexes:**
282
- - `formId` - Index for filtering by form
283
-
284
- **Note:** FormResult doesn't have `companyId` - company context is derived from the linked Form via `formId`. Company filtering is applied via JOIN in queries.
157
+ **Exported services** (available for injection after registration):
158
+ - `FormBuilderConfigService`
159
+ - `FormBuilderDataSourceProvider`
160
+ - `FormService`
161
+ - `FormResultService`
285
162
 
286
163
  ---
287
164
 
288
- ## DTOs
289
-
290
- ### Form DTOs
291
-
292
- | DTO | Purpose |
293
- |-----|---------|
294
- | `CreateFormDto` | Create new form |
295
- | `UpdateFormDto` | Update existing form |
296
- | `FormResponseDto` | Full form response |
297
- | `PublicFormResponseDto` | Limited fields for public access |
298
- | `FormAccessInfoResponseDto` | Access requirements info |
299
-
300
- #### CreateFormDto Fields
301
-
302
- ```typescript
303
- class CreateFormDto {
304
- name: string; // Required, max 255 chars
305
- description?: string; // Optional, max 500 chars
306
- slug?: string; // Optional, max 255 chars (unique)
307
- schema: Record<string, unknown>; // Required - form structure
308
- accessType?: FormAccessType; // Default: AUTHENTICATED
309
- actionGroups?: string[]; // For ACTION_GROUP access type
310
- companyId?: string; // When company feature enabled
311
- isActive?: boolean; // Default: true
312
- metadata?: Record<string, unknown>;
313
- }
314
- ```
315
-
316
- #### UpdateFormDto Fields
317
-
318
- ```typescript
319
- class UpdateFormDto extends PartialType(CreateFormDto) {
320
- id: string; // Required
321
- schemaVersion?: number; // Auto-incremented on schema change
322
- }
323
- ```
324
-
325
- ### Form Result DTOs
326
-
327
- | DTO | Purpose |
328
- |-----|---------|
329
- | `SubmitFormDto` | Public submission input |
330
- | `CreateFormResultDto` | Internal with extra fields |
331
- | `UpdateFormResultDto` | Update result |
332
- | `GetMyDraftDto` | Get user's draft for a form |
333
- | `UpdateDraftDto` | Update existing draft |
334
- | `GetResultsByFormDto` | Query results by form ID |
335
- | `FormResultResponseDto` | Result response |
336
-
337
- #### SubmitFormDto Fields
165
+ ## Configuration Reference
338
166
 
339
167
  ```typescript
340
- class SubmitFormDto {
341
- formId: string; // Required
342
- data: Record<string, unknown>; // Required - field values
343
- isDraft?: boolean; // Default: false
344
- metadata?: Record<string, unknown>;
168
+ interface IFormBuilderConfig extends IDataSourceServiceOptions {
169
+ // No form-builder-specific runtime config.
170
+ // IDataSourceServiceOptions provides:
171
+ // defaultDatabaseConfig?: IDatabaseConfig
172
+ // tenantDefaultDatabaseConfig?: IDatabaseConfig
173
+ // tenants?: ITenantDatabaseConfig[]
345
174
  }
346
175
  ```
347
176
 
348
- #### GetResultsByFormDto Fields
177
+ Bootstrap configuration:
349
178
 
350
- ```typescript
351
- class GetResultsByFormDto {
352
- formId: string; // Required
353
- page?: number; // Default: 0
354
- pageSize?: number; // Default: 10
355
- }
356
- ```
179
+ | Field | Type | Default | Effect |
180
+ |-------|------|---------|--------|
181
+ | `databaseMode` | `'single' \| 'multi-tenant'` | `'single'` | Controls DataSource resolution per request |
182
+ | `enableCompanyFeature` | `boolean` | `false` | Uses `FormWithCompany` entity when `true` |
357
183
 
358
184
  ---
359
185
 
360
- ## Services
361
-
362
- ### FormBuilderConfigService
363
-
364
- Provides access to module configuration:
186
+ ## Feature Toggles
365
187
 
366
- ```typescript
367
- @Injectable()
368
- export class FormBuilderConfigService implements IModuleConfigService {
369
- // Check if company feature is enabled (supports per-tenant override)
370
- isCompanyFeatureEnabled(tenant?: ITenantDatabaseConfig): boolean;
371
-
372
- // Get database mode ('single' | 'multi-tenant')
373
- getDatabaseMode(): DatabaseMode;
374
-
375
- // Check if running in multi-tenant mode
376
- isMultiTenant(): boolean;
377
-
378
- // Get full module options
379
- getOptions(): FormBuilderModuleOptions;
380
-
381
- // Get config section (defaultDatabaseConfig, tenants, etc.)
382
- getConfig(): IFormBuilderConfig | undefined;
383
- }
384
- ```
385
-
386
- ### FormService
387
-
388
- Extends `RequestScopedApiService` with form-specific operations:
389
-
390
- ```typescript
391
- @Injectable({ scope: Scope.REQUEST })
392
- export class FormService extends RequestScopedApiService<...> {
393
- // === Standard CRUD (inherited) ===
394
- // insert(dto, user), update(dto, user), delete(id, user), getAll(filter, user), getById(id, user)
395
-
396
- // === Public Access (no auth required) ===
397
-
398
- // Get form for public submission (accessType must be PUBLIC)
399
- async getPublicForm(formId: string): Promise<IPublicForm>;
400
-
401
- // Get public form by slug (no auth required)
402
- async getPublicFormBySlug(slug: string): Promise<IPublicForm | null>;
403
-
404
- // Get access info for routing decisions
405
- async getFormAccessInfo(formId: string): Promise<FormAccessInfoResponseDto>;
406
-
407
- // === Authenticated Access ===
408
-
409
- // Get form for authenticated submission (validates access + permissions)
410
- async getAuthenticatedForm(formId: string, user: ILoggedUserInfo): Promise<IPublicForm>;
411
-
412
- // Get form by slug (requires auth)
413
- async getBySlug(slug: string): Promise<IForm | null>;
414
-
415
- // Get form for submission (internal - validates access type)
416
- async getFormForSubmission(formId: string, user: ILoggedUserInfo | null): Promise<Form>;
417
- }
418
- ```
419
-
420
- **Schema Versioning:**
421
- - `schemaVersion` auto-increments when schema JSON changes
422
- - Comparison uses `JSON.stringify` for deep equality check
423
- - Version tracked in FormResult snapshots for historical accuracy
424
-
425
- ### FormResultService
426
-
427
- Handles form submissions and drafts:
428
-
429
- ```typescript
430
- @Injectable({ scope: Scope.REQUEST })
431
- export class FormResultService extends RequestScopedApiService<...> {
432
- // === Standard CRUD (inherited) ===
433
- // insert(dto, user), update(dto, user), delete(id, user), getAll(filter, user), getById(id, user)
434
-
435
- // === Form Submission ===
436
-
437
- // Submit form (validates access type, handles drafts)
438
- async submitForm(
439
- dto: SubmitFormDto,
440
- user: ILoggedUserInfo | null,
441
- isPublic?: boolean, // default: false
442
- ): Promise<IFormResult>;
443
-
444
- // === Draft Management ===
445
-
446
- // Get user's draft for a specific form
447
- async getMyDraft(formId: string, user: ILoggedUserInfo): Promise<IFormResult | null>;
448
-
449
- // Update existing draft (can convert to final submission)
450
- async updateDraft(
451
- draftId: string,
452
- dto: SubmitFormDto,
453
- user: ILoggedUserInfo,
454
- ): Promise<IFormResult>;
455
-
456
- // === Query Methods ===
457
-
458
- // Get results by form ID with pagination
459
- async getByFormId(
460
- formId: string,
461
- user: ILoggedUserInfo | null,
462
- pagination?: { page?: number; pageSize?: number },
463
- ): Promise<{ data: IFormResult[]; total: number }>;
464
-
465
- // Check if user has submitted (non-draft) for single response mode
466
- async hasUserSubmitted(formId: string, user: ILoggedUserInfo): Promise<boolean>;
467
- }
468
- ```
469
-
470
- **Key behaviors:**
471
- - Schema snapshot stored with each submission for historical accuracy
472
- - Drafts auto-update if user re-submits as draft
473
- - Final submission deletes existing draft (soft delete)
474
- - Computed fields applied only on final submission (not drafts)
475
- - Company filtering via JOIN when company feature enabled
476
-
477
- ### FormBuilderDataSourceProvider
478
-
479
- Extends `MultiTenantDataSourceService` for dynamic entity loading:
480
-
481
- ```typescript
482
- @Injectable({ scope: Scope.REQUEST })
483
- export class FormBuilderDataSourceProvider extends MultiTenantDataSourceService {
484
- // Maintains separate static cache from other modules
485
- protected static readonly tenantConnections = new Map<string, DataSource>();
486
- protected static singleDataSource: DataSource | null = null;
487
-
488
- // Get entities based on company feature flag
489
- async getFormBuilderEntities(enableCompanyFeature?: boolean): Promise<any[]>;
490
-
491
- // Inherited from MultiTenantDataSourceService
492
- async getDataSource(): Promise<DataSource>;
493
- async getRepository<T>(entity: EntityTarget<T>): Promise<Repository<T>>;
494
- }
495
- ```
188
+ | Feature | Config | Default | Effect |
189
+ |---------|--------|---------|--------|
190
+ | Company scoping | `enableCompanyFeature: true` | `false` | All queries filtered by `companyId` from JWT; uses `FormWithCompany` entity |
191
+ | Multi-tenant | `databaseMode: 'multi-tenant'` | `'single'` | Per-tenant DataSource connections |
496
192
 
497
193
  ---
498
194
 
499
- ## Controllers
195
+ ## API Endpoints
500
196
 
501
- ### FormController
197
+ All endpoints use **POST**. Authentication depends on the form's `accessType`.
502
198
 
503
- Base path: `/form-builder/form`
199
+ ### Forms — `POST /form-builder/form/*`
504
200
 
505
201
  | Endpoint | Auth | Description |
506
202
  |----------|------|-------------|
507
- | `POST /insert` | JWT | Create form |
508
- | `POST /update` | JWT | Update form |
509
- | `POST /delete` | JWT | Delete form |
510
- | `POST /get-all` | JWT | List forms |
511
- | `POST /get/:id` | JWT | Get form by ID |
512
- | `POST /access-info/:id` | Public | Get access requirements |
513
- | `POST /public/:id` | Public | Get public form |
514
- | `POST /authenticated/:id` | JWT | Get authenticated form |
515
- | `POST /by-slug/:slug` | JWT | Get form by slug |
516
- | `POST /public/by-slug/:slug` | Public | Get public form by slug |
203
+ | `POST /form-builder/form/insert` | `form.create` | Create a new form with JSON schema |
204
+ | `POST /form-builder/form/get-all` | `form.read` | List all forms (admin) |
205
+ | `POST /form-builder/form/get/:id` | `form.read` | Get form by ID |
206
+ | `POST /form-builder/form/get-by-slug/:slug` | Varies* | Get form by URL slug |
207
+ | `POST /form-builder/form/update` | `form.update` | Update form (increments schema version if schema changes) |
208
+ | `POST /form-builder/form/delete` | `form.delete` | Delete form |
209
+ | `POST /form-builder/form/publish` | `form.update` | Publish draft form |
210
+ | `POST /form-builder/form/unpublish` | `form.update` | Unpublish form |
517
211
 
518
- ### FormResultController
212
+ *`get-by-slug` respects the form's `accessType` — PUBLIC forms are accessible without auth.
519
213
 
520
- Base path: `/form-builder/result`
214
+ ### Form Results (Submissions) — `POST /form-builder/result/*`
521
215
 
522
216
  | Endpoint | Auth | Description |
523
217
  |----------|------|-------------|
524
- | `POST /insert` | JWT | Create result (internal) |
525
- | `POST /update` | JWT | Update result |
526
- | `POST /delete` | JWT | Delete result |
527
- | `POST /get-all` | JWT | List results |
528
- | `POST /get/:id` | JWT | Get result by ID |
529
- | `POST /submit` | JWT | Submit form (authenticated) |
530
- | `POST /submit-public` | Public | Submit form (public) |
531
- | `POST /my-draft` | JWT | Get user's draft for a form |
532
- | `POST /update-draft` | JWT | Update draft or convert to final |
533
- | `POST /by-form` | JWT | Get results by form ID |
534
- | `POST /has-submitted` | JWT | Check if user has submitted |
218
+ | `POST /form-builder/result/insert` | Varies* | Submit a form result |
219
+ | `POST /form-builder/result/get-all` | `form-result.read` | List all submissions |
220
+ | `POST /form-builder/result/get/:id` | `form-result.read` | Get submission by ID |
221
+ | `POST /form-builder/result/update` | `form-result.update` | Update a submission |
222
+ | `POST /form-builder/result/delete` | `form-result.delete` | Delete a submission |
223
+ | `POST /form-builder/result/save-draft` | Varies* | Save a partial draft submission |
224
+ | `POST /form-builder/result/finalize` | Varies* | Convert draft to final submission |
225
+ | `POST /form-builder/result/get-my-results` | JWT | Get current user's own submissions |
535
226
 
536
- ---
537
-
538
- ## Access Control
227
+ *Depends on the form's `accessType`.
539
228
 
540
- ### Access Types
229
+ ---
541
230
 
542
- | Type | Description | Endpoint |
543
- |------|-------------|----------|
544
- | `public` | No authentication | `submit-public` |
545
- | `authenticated` | Login required | `submit` |
546
- | `action_group` | Specific permissions | `submit` + permission check |
231
+ ## Entities
547
232
 
548
- ### Flow
233
+ ### Core Entities (always registered)
549
234
 
550
- 1. Frontend calls `access-info/:id` to determine requirements
551
- 2. Based on `accessType`:
552
- - `public` Fetch via `public/:id`, submit via `submit-public`
553
- - `authenticated` Redirect to login if needed, use `authenticated/:id`, submit via `submit`
554
- - `action_group` → Same as authenticated + permission check on submit
235
+ | Entity | Table | Description |
236
+ |--------|-------|-------------|
237
+ | `Form` | `form_builder_form` | Form definition with JSON schema, access type, version |
238
+ | `FormResult` | `form_builder_result` | Submission data + schema version snapshot |
555
239
 
556
- ### Permission Checking
240
+ ### Company Feature Entities (`enableCompanyFeature: true`)
557
241
 
558
- For `action_group` forms:
559
- - Form stores required permissions in `actionGroups` array
560
- - Service checks if user has ANY of the listed permissions
561
- - Uses cache-based permission lookup via `validateUserPermissions`
242
+ | Entity | Table | Description |
243
+ |--------|-------|-------------|
244
+ | `FormWithCompany` | `form_builder_form` | Same as Form + `companyId` |
562
245
 
563
246
  ```typescript
564
- import { validateUserPermissions } from '@flusys/nestjs-form-builder';
565
-
566
- // In FormService.getAuthenticatedForm()
567
- if (form.accessType === FormAccessType.ACTION_GROUP && form.actionGroups?.length) {
568
- const hasPermission = await validateUserPermissions(
569
- user,
570
- form.actionGroups,
571
- this.cacheManager,
572
- this.formBuilderConfig.isCompanyFeatureEnabled(),
573
- this.logger,
574
- 'accessing form',
575
- form.id,
576
- );
577
- if (!hasPermission) {
578
- throw new ForbiddenException('You do not have permission to access this form');
579
- }
580
- }
581
- ```
582
-
583
- ---
584
-
585
- ## Multi-Tenant Support
586
-
587
- ### Configuration
247
+ import { FormBuilderModule } from '@flusys/nestjs-form-builder';
588
248
 
589
- ```typescript
590
- FormBuilderModule.forRoot({
591
- bootstrapAppConfig: {
592
- enableCompanyFeature: true,
593
- },
249
+ TypeOrmModule.forRoot({
250
+ entities: [
251
+ ...FormBuilderModule.getEntities({ enableCompanyFeature: true }),
252
+ ],
594
253
  })
595
254
  ```
596
255
 
597
- ### Entity Selection
598
-
599
- The module automatically selects the correct entity:
600
- - `enableCompanyFeature: false` → `Form` + `FormResult`
601
- - `enableCompanyFeature: true` → `FormWithCompany` + `FormResult`
602
-
603
- ### Company Filtering
604
-
605
- When company feature is enabled:
606
- - Forms are filtered by `user.companyId`
607
- - Results are filtered via JOIN to Form's `companyId`
608
- - New forms get `companyId` from user context or DTO
609
-
610
- ### DataSource Provider
611
-
612
- `FormBuilderDataSourceProvider` extends `MultiTenantDataSourceService`:
613
- - Maintains separate static cache from other modules
614
- - Dynamically loads correct entities per tenant
615
- - Supports per-tenant feature flags
616
-
617
256
  ---
618
257
 
619
- ## API Reference
620
-
621
- ### Module Options
622
-
623
- ```typescript
624
- interface FormBuilderModuleOptions extends IDynamicModuleConfig {
625
- global?: boolean; // Make module global
626
- includeController?: boolean; // Include REST controllers
627
- bootstrapAppConfig?: IBootstrapAppConfig; // Bootstrap configuration
628
- config?: IFormBuilderConfig; // Form builder configuration
629
- }
630
-
631
- interface IFormBuilderConfig extends IDataSourceServiceOptions {
632
- // Currently no form-builder specific runtime config
633
- // Add form-builder specific settings here as needed
634
- defaultDatabaseConfig?: IDatabaseConfig;
635
- tenantDefaultDatabaseConfig?: IDatabaseConfig;
636
- tenants?: ITenantDatabaseConfig[];
637
- }
638
- ```
639
-
640
- ### Async Options
641
-
642
- ```typescript
643
- interface FormBuilderModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
644
- global?: boolean;
645
- includeController?: boolean;
646
- bootstrapAppConfig?: IBootstrapAppConfig;
647
- useFactory?: (...args: any[]) => Promise<IFormBuilderConfig> | IFormBuilderConfig;
648
- inject?: any[];
649
- useExisting?: Type<FormBuilderOptionsFactory>;
650
- useClass?: Type<FormBuilderOptionsFactory>;
651
- }
258
+ ## Access Control
652
259
 
653
- interface FormBuilderOptionsFactory {
654
- createFormBuilderOptions(): Promise<IFormBuilderConfig> | IFormBuilderConfig;
655
- }
656
- ```
260
+ Each form has an `accessType` field that controls who can access it:
657
261
 
658
- ### Interfaces
262
+ | Access Type | Description |
263
+ |-------------|-------------|
264
+ | `PUBLIC` | No authentication required. Anyone can view and submit. |
265
+ | `AUTHENTICATED` | Requires valid JWT token. Any logged-in user can submit. |
266
+ | `ACTION_GROUP` | Requires a specific IAM action permission (from `nestjs-iam`). |
659
267
 
660
- ```typescript
661
- interface IForm {
662
- id: string;
663
- name: string;
664
- description: string | null;
665
- slug: string | null;
666
- schema: Record<string, unknown>;
667
- schemaVersion: number;
668
- accessType: FormAccessType;
669
- actionGroups: string[] | null;
670
- isActive: boolean;
671
- companyId: string | null;
672
- metadata: Record<string, unknown> | null;
673
- createdAt: Date;
674
- updatedAt: Date;
675
- deletedAt: Date | null;
676
- createdById: string | null;
677
- updatedById: string | null;
678
- deletedById: string | null;
679
- }
680
-
681
- interface IFormResult {
682
- id: string;
683
- formId: string;
684
- schemaVersionSnapshot: Record<string, unknown>;
685
- schemaVersion: number;
686
- data: Record<string, unknown>;
687
- submittedById: string | null;
688
- submittedAt: Date;
689
- isDraft: boolean;
690
- metadata: Record<string, unknown> | null;
691
- // ... audit fields
692
- }
268
+ **Creating a form with access control:**
693
269
 
694
- interface IPublicForm {
695
- id: string;
696
- name: string;
697
- description: string | null;
698
- schema: Record<string, unknown>;
699
- schemaVersion: number;
270
+ ```json
271
+ POST /form-builder/form/insert
272
+ {
273
+ "name": "Employee Survey",
274
+ "slug": "employee-survey",
275
+ "accessType": "AUTHENTICATED",
276
+ "schema": { /* JSON schema */ },
277
+ "isPublished": true
700
278
  }
701
279
  ```
702
280
 
703
- ### Enums
281
+ **ACTION_GROUP form:**
704
282
 
705
- ```typescript
706
- enum FormAccessType {
707
- PUBLIC = 'public',
708
- AUTHENTICATED = 'authenticated',
709
- ACTION_GROUP = 'action_group',
283
+ ```json
284
+ {
285
+ "name": "Finance Report",
286
+ "slug": "finance-report",
287
+ "accessType": "ACTION_GROUP",
288
+ "requiredAction": "finance.submit-report",
289
+ "schema": { /* JSON schema */ }
710
290
  }
711
291
  ```
712
292
 
713
293
  ---
714
294
 
715
- ## Computed Fields
716
-
717
- Computed fields are values automatically calculated from form responses when a form is submitted. The calculation happens server-side before storing the result.
718
-
719
- ### How It Works
295
+ ## Schema Versioning
720
296
 
721
- 1. Form schema contains `computedFields` in settings
722
- 2. On final submission (not drafts), backend calculates values
723
- 3. Computed values are stored in `data._computed` namespace
724
- 4. Original field values remain unchanged
297
+ When you update a form's `schema` field, the `schemaVersion` counter is automatically incremented. Every form submission stores the `schemaVersion` at the time of submission.
725
298
 
726
- ### Utility Functions
299
+ This allows:
300
+ - Viewing historical submissions against the exact schema that was active
301
+ - Running analytics on form data across schema versions
302
+ - Preventing schema changes from breaking existing submission records
727
303
 
728
- ```typescript
729
- import { calculateComputedFields, IComputedField } from '@flusys/nestjs-form-builder';
304
+ ```json
305
+ // Version 1 schema
306
+ { "fields": [{ "name": "firstName", "type": "text" }] }
730
307
 
731
- // Calculate computed fields from form data
732
- const computedValues = calculateComputedFields(formData, computedFields);
733
- // Result: { total_score: 15, category: 'premium' }
308
+ // After update (version 2) - new field added
309
+ { "fields": [{ "name": "firstName", "type": "text" }, { "name": "lastName", "type": "text" }] }
734
310
  ```
735
311
 
736
- **Note:** Backend interface definitions mirror `@flusys/ng-form-builder` interfaces but are defined separately to avoid cross-package dependencies. Both use compatible JSON structures for serialization. See `computed-field.utils.ts` for implementation.
312
+ Historical submissions still reference `schemaVersion: 1` so you know they were submitted before the `lastName` field existed.
737
313
 
738
- ### Computed Field Interfaces
739
-
740
- ```typescript
741
- interface IComputedField {
742
- id: string;
743
- name: string;
744
- key: string; // Storage key in _computed
745
- valueType: 'string' | 'number';
746
- rules: IComputedRule[];
747
- defaultValue?: string | number | null;
748
- description?: string;
749
- }
314
+ ---
750
315
 
751
- interface IComputedRule {
752
- id: string;
753
- condition?: IComputedConditionGroup; // Optional - no condition = always apply
754
- computation: IComputation;
755
- }
316
+ ## Draft Support
756
317
 
757
- interface IComputedConditionGroup {
758
- operator: 'AND' | 'OR';
759
- conditions: IComputedCondition[];
760
- }
318
+ Users can save partial form data as a draft and finalize later:
761
319
 
762
- interface IComputedCondition {
763
- fieldId: string;
764
- comparison: string; // See Condition Operators below
765
- value: unknown;
320
+ ```json
321
+ // Save draft (no validation on all required fields)
322
+ POST /form-builder/result/save-draft
323
+ {
324
+ "formId": "uuid",
325
+ "data": { "firstName": "John" }
766
326
  }
767
327
 
768
- interface IComputation {
769
- type: ComputationType;
770
- config: IDirectValueConfig | IFieldReferenceConfig | IArithmeticConfig;
328
+ // Finalize (validates all required fields)
329
+ POST /form-builder/result/finalize
330
+ {
331
+ "draftId": "uuid",
332
+ "data": { "firstName": "John", "lastName": "Doe" }
771
333
  }
772
-
773
- type ComputationType = 'direct' | 'field_reference' | 'arithmetic';
774
334
  ```
775
335
 
776
- ### Computation Config Types
336
+ Draft submissions have `isDraft: true`. Finalized submissions have `isDraft: false`. Users can retrieve their own drafts via `POST /form-builder/result/get-my-results`.
777
337
 
778
- ```typescript
779
- // Direct value - set a static value
780
- interface IDirectValueConfig {
781
- type: 'direct';
782
- value: string | number;
783
- }
784
-
785
- // Field reference - copy value from another field
786
- interface IFieldReferenceConfig {
787
- type: 'field_reference';
788
- fieldId: string;
789
- }
790
-
791
- // Arithmetic - calculate from multiple operands
792
- interface IArithmeticConfig {
793
- type: 'arithmetic';
794
- operation: ArithmeticOperation;
795
- operands: IArithmeticOperand[];
796
- }
338
+ ---
797
339
 
798
- interface IArithmeticOperand {
799
- type: 'field' | 'constant';
800
- fieldId?: string; // When type = 'field'
801
- value?: number; // When type = 'constant'
802
- }
803
- ```
340
+ ## Computed Fields Engine
804
341
 
805
- ### Supported Operations
806
-
807
- | Type | Description |
808
- |------|-------------|
809
- | `direct` | Set a static value |
810
- | `field_reference` | Copy value from another field |
811
- | `arithmetic` | Calculate using arithmetic operations |
812
-
813
- ### Arithmetic Operations
814
-
815
- | Operation | Description |
816
- |-----------|-------------|
817
- | `sum` | Add all operand values |
818
- | `subtract` | Subtract subsequent values from first |
819
- | `multiply` | Multiply all operand values |
820
- | `divide` | Divide first value by subsequent values |
821
- | `average` | Calculate average of all operands |
822
- | `min` | Get minimum value |
823
- | `max` | Get maximum value |
824
- | `increment` | Alias for sum |
825
- | `decrement` | Alias for subtract |
826
-
827
- ### Condition Operators
828
-
829
- Computed fields support conditional rules with these comparison operators:
830
-
831
- | Operator | Aliases | Description |
832
- |----------|---------|-------------|
833
- | `equals` | | Value equality (string-safe) |
834
- | `not_equals` | | Value inequality |
835
- | `contains` | | String contains substring |
836
- | `not_contains` | | String does not contain substring |
837
- | `starts_with` | | String starts with value |
838
- | `ends_with` | | String ends with value |
839
- | `greater_than` | | Numeric greater than |
840
- | `less_than` | | Numeric less than |
841
- | `greater_or_equal` | | Numeric greater or equal |
842
- | `less_or_equal` | | Numeric less or equal |
843
- | `is_empty` | | Null, undefined, empty string, or empty array |
844
- | `is_not_empty` | | Has a non-empty value |
845
- | `is_before` | | Date comparison (before) |
846
- | `is_after` | | Date comparison (after) |
847
- | `is_checked` | | Boolean true (or 'true', or 1) |
848
- | `is_not_checked` | | Boolean false (or 'false', 0, falsy) |
849
- | `is_any_of` | `in` | Value is in array |
850
- | `is_none_of` | `not_in` | Value is not in array |
851
-
852
- ### Data Storage
853
-
854
- Submission data includes computed values:
342
+ Server-side computed fields are calculated from submission data using declarative rules:
855
343
 
856
344
  ```json
345
+ POST /form-builder/form/insert
857
346
  {
858
- "formId": "uuid",
859
- "data": {
860
- "name": "John",
861
- "rating": 5,
862
- "_computed": {
863
- "total_score": 100,
864
- "satisfaction_level": "high"
865
- }
347
+ "name": "Loan Calculator",
348
+ "schema": {
349
+ "fields": [
350
+ { "name": "principal", "type": "number", "label": "Loan Amount" },
351
+ { "name": "rate", "type": "number", "label": "Interest Rate (%)" },
352
+ { "name": "years", "type": "number", "label": "Term (Years)" }
353
+ ],
354
+ "computedFields": [
355
+ {
356
+ "name": "monthlyPayment",
357
+ "label": "Monthly Payment",
358
+ "expression": "(principal * (rate / 1200)) / (1 - Math.pow(1 + rate / 1200, -years * 12))",
359
+ "dependsOn": ["principal", "rate", "years"]
360
+ },
361
+ {
362
+ "name": "totalPayment",
363
+ "label": "Total Payment",
364
+ "expression": "monthlyPayment * years * 12",
365
+ "dependsOn": ["monthlyPayment", "years"]
366
+ }
367
+ ]
866
368
  }
867
369
  }
868
370
  ```
869
371
 
870
- ### Integration in Service
871
-
872
- The `FormResultService` automatically calculates computed fields on submission via a private helper:
873
-
874
- ```typescript
875
- // Private method in FormResultService
876
- private applyComputedFields(
877
- data: Record<string, unknown>,
878
- form: Form,
879
- isDraft: boolean,
880
- ): Record<string, unknown> {
881
- if (isDraft) return data;
882
-
883
- const schema = form.schema as Record<string, unknown>;
884
- const settings = schema?.settings as Record<string, unknown> | undefined;
885
- const computedFields = settings?.computedFields as IComputedField[] | undefined;
886
-
887
- if (!computedFields || computedFields.length === 0) return data;
888
-
889
- const computedValues = calculateComputedFields(data, computedFields);
890
- return { ...data, _computed: computedValues };
891
- }
892
-
893
- // Used in submitForm() and updateDraft()
894
- const finalData = this.applyComputedFields(dto.data, form, isDraft);
895
- ```
372
+ Computed values are evaluated server-side at submission time and stored with the result.
896
373
 
897
374
  ---
898
375
 
899
- ## Response Mode
376
+ ## Exported Services
900
377
 
901
- The form schema supports a `responseMode` setting that controls whether users can submit multiple responses.
378
+ | Service | Description |
379
+ |---------|-------------|
380
+ | `FormService` | Form CRUD, slug lookup, publish/unpublish |
381
+ | `FormResultService` | Submission CRUD, draft management, computed field evaluation |
382
+ | `FormBuilderConfigService` | Exposes runtime config and feature flags |
383
+ | `FormBuilderDataSourceProvider` | Dynamic DataSource resolution per request |
902
384
 
903
- ### Settings
904
-
905
- | Mode | Description |
906
- |------|-------------|
907
- | `multiple` | Default. Users can submit unlimited responses |
908
- | `single` | Each user can only submit once |
385
+ ---
909
386
 
910
- ### Tracking by Access Type
387
+ ## Programmatic Usage
911
388
 
912
- | Access Type | Tracking Method | Reliability |
913
- |-------------|-----------------|-------------|
914
- | `authenticated` | Server-side via `submittedById` | Reliable |
915
- | `action_group` | Server-side via `submittedById` | Reliable |
916
- | `public` | Client-side (frontend handles) | Best-effort only |
389
+ ```typescript
390
+ import { FormService, FormResultService } from '@flusys/nestjs-form-builder';
917
391
 
918
- ### Backend Endpoint
392
+ @Injectable()
393
+ export class SurveyService {
394
+ constructor(
395
+ @Inject(FormService) private readonly formService: FormService,
396
+ @Inject(FormResultService) private readonly resultService: FormResultService,
397
+ ) {}
398
+
399
+ async getPublicForm(slug: string) {
400
+ return this.formService.getBySlug(slug);
401
+ }
919
402
 
920
- For authenticated forms, the frontend calls `hasUserSubmitted` to check if the user has already submitted:
403
+ async submitForm(formId: string, data: Record<string, any>, userId: string) {
404
+ return this.resultService.submit({ formId, data, userId });
405
+ }
921
406
 
922
- ```typescript
923
- // FormResultController - POST /form-builder/result/has-submitted
924
- @Post('has-submitted')
925
- @UseGuards(JwtAuthGuard)
926
- async hasUserSubmitted(
927
- @Body() dto: GetMyDraftDto, // { formId: string }
928
- @CurrentUser() user: ILoggedUserInfo,
929
- ): Promise<boolean> {
930
- return this.formResultService.hasUserSubmitted(dto.formId, user);
407
+ async getFormResults(formId: string) {
408
+ return this.resultService.getAll({ filter: { formId } }, null);
409
+ }
931
410
  }
932
411
  ```
933
412
 
934
- **Note:** Public forms cannot be reliably tracked server-side since there's no user identity. The frontend uses `localStorage` as a best-effort solution, but this can be bypassed. For strict single-response enforcement, use `authenticated` or `action_group` access type.
935
-
936
413
  ---
937
414
 
938
- ## Best Practices
939
-
940
- ### Schema Design
941
-
942
- - Store complete form schema including sections, fields, and settings
943
- - Use schema versioning to track changes
944
- - Store schema snapshots with results for historical accuracy
945
-
946
- ### Access Control
947
-
948
- - Use `public` sparingly - only for truly anonymous forms
949
- - Prefer `authenticated` for most internal forms
950
- - Use `action_group` for sensitive forms requiring specific permissions
951
-
952
- ### Company Isolation
953
-
954
- - Always set `companyId` when company feature is enabled
955
- - Use user's company context as default
956
- - Allow explicit `companyId` in DTO for admin operations
415
+ ## Troubleshooting
957
416
 
958
- ### Performance
417
+ **`Form not found` for a public form**
959
418
 
960
- - Use pagination when fetching results
961
- - Select only needed fields in queries
962
- - Consider caching frequently accessed forms
419
+ Check that the form is published (`isPublished: true`). Unpublished forms are not returned by the public API.
963
420
 
964
421
  ---
965
422
 
966
- ## Swagger Configuration
967
-
968
- The package includes a Swagger configuration helper that adapts documentation based on feature flags:
969
-
970
- ```typescript
971
- import { formBuilderSwaggerConfig } from '@flusys/nestjs-form-builder';
972
- import { setupSwaggerDocs } from '@flusys/nestjs-core/docs';
973
-
974
- // In bootstrap
975
- const bootstrapConfig = { enableCompanyFeature: true };
976
- setupSwaggerDocs(app, formBuilderSwaggerConfig(bootstrapConfig));
977
- ```
978
-
979
- **Features:**
980
- - Automatically excludes `companyId` fields when company feature is disabled
981
- - Generates comprehensive API documentation
982
- - Documents all access types and workflows
983
-
984
- ### Schema Exclusions
423
+ **Draft finalization fails validation**
985
424
 
986
- When `enableCompanyFeature: false`, these schemas hide `companyId`:
987
- - `CreateFormDto`
988
- - `UpdateFormDto`
989
- - `FormQueryDto`
990
- - `FormResponseDto`
425
+ The `finalize` endpoint applies full schema validation. Ensure all required fields in the form schema are present in the submitted data.
991
426
 
992
427
  ---
993
428
 
994
- ## Permission Utilities
429
+ **Computed fields not appearing in submission**
995
430
 
996
- The package provides permission validation utilities for action group access:
431
+ Computed fields are evaluated at submission time. Check that `dependsOn` field names exactly match the form field `name` values (case-sensitive).
997
432
 
998
- ```typescript
999
- import { validateUserPermissions } from '@flusys/nestjs-form-builder';
1000
-
1001
- // Validate user has at least one of the required permissions
1002
- const hasPermission = await validateUserPermissions(
1003
- user, // ILoggedUserInfo
1004
- ['hr.survey.submit', 'admin'], // Required permissions (OR logic)
1005
- cacheManager, // HybridCache instance
1006
- enableCompanyFeature, // boolean
1007
- logger, // Logger instance
1008
- 'submitting form', // Context for audit logging
1009
- formId, // Resource ID for audit logging
1010
- );
1011
- ```
1012
-
1013
- **Features:**
1014
- - Reads permissions from cache (same format as PermissionGuard)
1015
- - Fail-closed behavior: cache errors result in access denial
1016
- - Audit logging for permission denials
433
+ ---
1017
434
 
1018
- ### Permission Cache Key Format
435
+ **`No metadata for entity`**
1019
436
 
437
+ Register entities in your `TypeOrmModule`:
1020
438
  ```typescript
1021
- // With company feature enabled
1022
- `permissions:company:${companyId}:branch:${branchId}:user:${userId}`
1023
-
1024
- // Without company feature
1025
- `permissions:user:${userId}`
439
+ entities: [...FormBuilderModule.getEntities({ enableCompanyFeature: false })]
1026
440
  ```
1027
441
 
1028
442
  ---
1029
443
 
1030
- ## Controller Security
1031
-
1032
- Both controllers use `createApiController` with permission-based security:
1033
-
1034
- ### Form Permissions
1035
-
1036
- | Operation | Permission |
1037
- |-----------|------------|
1038
- | Create | `FORM_PERMISSIONS.CREATE` |
1039
- | Read | `FORM_PERMISSIONS.READ` |
1040
- | Update | `FORM_PERMISSIONS.UPDATE` |
1041
- | Delete | `FORM_PERMISSIONS.DELETE` |
1042
-
1043
- ### Form Result Permissions
1044
-
1045
- | Operation | Permission |
1046
- |-----------|------------|
1047
- | Create | `FORM_RESULT_PERMISSIONS.CREATE` |
1048
- | Read | `FORM_RESULT_PERMISSIONS.READ` |
1049
- | Update | `FORM_RESULT_PERMISSIONS.UPDATE` |
1050
- | Delete | `FORM_RESULT_PERMISSIONS.DELETE` |
1051
-
1052
- **Note:** Submit endpoints (`submit`, `submit-public`) don't require these permissions - they use the form's `accessType` for authorization.
1053
-
1054
- ---
1055
-
1056
- ## See Also
444
+ ## License
1057
445
 
1058
- - [ng-form-builder Guide](../../FLUSYS_NG/docs/FORM-BUILDER-GUIDE.md) - Frontend components
1059
- - [Shared Guide](SHARED-GUIDE.md) - Base classes and utilities
1060
- - [Auth Guide](AUTH-GUIDE.md) - User and company management
446
+ MIT © FLUSYS
1061
447
 
1062
448
  ---
1063
449
 
1064
- **Last Updated:** 2026-02-25
450
+ > Part of the **FLUSYS** framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.