@flusys/nestjs-form-builder 1.1.0-beta → 2.0.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.
Files changed (54) hide show
  1. package/README.md +722 -0
  2. package/cjs/controllers/form-result.controller.js +67 -5
  3. package/cjs/controllers/form.controller.js +48 -15
  4. package/cjs/docs/form-builder-swagger.config.js +6 -100
  5. package/cjs/dtos/form-result.dto.js +6 -93
  6. package/cjs/dtos/form.dto.js +21 -163
  7. package/cjs/entities/form-with-company.entity.js +12 -2
  8. package/cjs/entities/form.entity.js +103 -3
  9. package/cjs/entities/index.js +28 -16
  10. package/cjs/index.js +1 -0
  11. package/cjs/interfaces/form-result.interface.js +1 -6
  12. package/cjs/modules/form-builder.module.js +57 -83
  13. package/cjs/services/form-builder-config.service.js +6 -16
  14. package/cjs/services/form-builder-datasource.provider.js +19 -59
  15. package/cjs/services/form-result.service.js +107 -181
  16. package/cjs/services/form.service.js +56 -72
  17. package/cjs/utils/computed-field.utils.js +17 -29
  18. package/cjs/utils/permission.utils.js +11 -16
  19. package/controllers/form-result.controller.d.ts +10 -12
  20. package/dtos/form-result.dto.d.ts +2 -19
  21. package/dtos/form.dto.d.ts +6 -32
  22. package/entities/form-with-company.entity.d.ts +2 -2
  23. package/entities/form.entity.d.ts +12 -2
  24. package/entities/index.d.ts +7 -2
  25. package/fesm/controllers/form-result.controller.js +69 -7
  26. package/fesm/controllers/form.controller.js +50 -17
  27. package/fesm/docs/form-builder-swagger.config.js +6 -100
  28. package/fesm/dtos/form-result.dto.js +9 -99
  29. package/fesm/dtos/form.dto.js +22 -165
  30. package/fesm/entities/form-with-company.entity.js +12 -2
  31. package/fesm/entities/form.entity.js +104 -4
  32. package/fesm/entities/index.js +18 -24
  33. package/fesm/index.js +2 -0
  34. package/fesm/modules/form-builder.module.js +57 -83
  35. package/fesm/services/form-builder-config.service.js +6 -16
  36. package/fesm/services/form-builder-datasource.provider.js +19 -59
  37. package/fesm/services/form-result.service.js +107 -181
  38. package/fesm/services/form.service.js +56 -72
  39. package/fesm/utils/computed-field.utils.js +17 -29
  40. package/fesm/utils/permission.utils.js +2 -9
  41. package/index.d.ts +1 -0
  42. package/interfaces/form-builder-module.interface.d.ts +4 -7
  43. package/interfaces/form-result.interface.d.ts +2 -9
  44. package/interfaces/form.interface.d.ts +2 -10
  45. package/modules/form-builder.module.d.ts +4 -3
  46. package/package.json +3 -3
  47. package/services/form-builder-config.service.d.ts +5 -3
  48. package/services/form-builder-datasource.provider.d.ts +3 -6
  49. package/services/form-result.service.d.ts +5 -0
  50. package/services/form.service.d.ts +13 -10
  51. package/utils/permission.utils.d.ts +0 -2
  52. package/cjs/entities/form-base.entity.js +0 -113
  53. package/entities/form-base.entity.d.ts +0 -13
  54. package/fesm/entities/form-base.entity.js +0 -106
package/README.md ADDED
@@ -0,0 +1,722 @@
1
+ # Form Builder Package Guide
2
+
3
+ > **Package:** `@flusys/nestjs-form-builder`
4
+ > **Type:** Dynamic form management with schema versioning and access control
5
+
6
+ This guide covers the NestJS form builder package - dynamic form creation, submission storage, and multi-tenant support.
7
+
8
+ ## Table of Contents
9
+
10
+ - [Overview](#overview)
11
+ - [Installation](#installation)
12
+ - [Constants](#constants)
13
+ - [Package Architecture](#package-architecture)
14
+ - [Module Setup](#module-setup)
15
+ - [Entities](#entities)
16
+ - [DTOs](#dtos)
17
+ - [Services](#services)
18
+ - [Controllers](#controllers)
19
+ - [Access Control](#access-control)
20
+ - [Multi-Tenant Support](#multi-tenant-support)
21
+ - [API Reference](#api-reference)
22
+ - [Computed Fields](#computed-fields)
23
+ - [Response Mode](#response-mode)
24
+ - [Best Practices](#best-practices)
25
+
26
+ ---
27
+
28
+ ## Overview
29
+
30
+ `@flusys/nestjs-form-builder` provides a comprehensive form management system:
31
+
32
+ - **Dynamic Forms** - JSON schema-based form definitions
33
+ - **Schema Versioning** - Auto-increment version on schema changes
34
+ - **Result Snapshots** - Store schema at submission time for historical accuracy
35
+ - **Access Control** - Public, authenticated, and permission-based access
36
+ - **Multi-Tenant Support** - Optional company isolation
37
+ - **POST-only RPC** - Follows project API conventions
38
+
39
+ ### Package Hierarchy
40
+
41
+ ```
42
+ @flusys/nestjs-core ← Foundation
43
+
44
+ @flusys/nestjs-shared ← Shared utilities
45
+
46
+ @flusys/nestjs-form-builder ← Form management (THIS PACKAGE)
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ npm install @flusys/nestjs-form-builder @flusys/nestjs-shared @flusys/nestjs-core
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Constants
60
+
61
+ ```typescript
62
+ // Injection Token
63
+ export const FORM_BUILDER_MODULE_OPTIONS = 'FORM_BUILDER_MODULE_OPTIONS';
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Package Architecture
69
+
70
+ ```
71
+ nestjs-form-builder/
72
+ ├── src/
73
+ │ ├── modules/
74
+ │ │ └── form-builder.module.ts # Main module
75
+ │ │
76
+ │ ├── config/
77
+ │ │ ├── form-builder.constants.ts # Constants
78
+ │ │ └── index.ts
79
+ │ │
80
+ │ ├── entities/
81
+ │ │ ├── form.entity.ts # Main form entity
82
+ │ │ ├── form-with-company.entity.ts # Extends Form with company
83
+ │ │ ├── form-result.entity.ts # Submissions
84
+ │ │ └── index.ts
85
+ │ │
86
+ │ ├── dtos/
87
+ │ │ ├── form.dto.ts # Form DTOs
88
+ │ │ ├── form-result.dto.ts # Result DTOs
89
+ │ │ └── index.ts
90
+ │ │
91
+ │ ├── services/
92
+ │ │ ├── form-builder-config.service.ts # Config service
93
+ │ │ ├── form-builder-datasource.provider.ts
94
+ │ │ ├── form.service.ts # Form CRUD
95
+ │ │ ├── form-result.service.ts # Submission handling
96
+ │ │ └── index.ts
97
+ │ │
98
+ │ ├── controllers/
99
+ │ │ ├── form.controller.ts # Form endpoints
100
+ │ │ ├── form-result.controller.ts # Result endpoints
101
+ │ │ └── index.ts
102
+ │ │
103
+ │ ├── enums/
104
+ │ │ ├── form-access-type.enum.ts
105
+ │ │ └── index.ts
106
+ │ │
107
+ │ ├── interfaces/
108
+ │ │ ├── form.interface.ts
109
+ │ │ ├── form-result.interface.ts
110
+ │ │ ├── form-builder-module.interface.ts
111
+ │ │ └── index.ts
112
+ │ │
113
+ │ ├── utils/
114
+ │ │ ├── permission.utils.ts # Permission validation
115
+ │ │ ├── computed-field.utils.ts # Computed field calculation
116
+ │ │ └── index.ts
117
+ │ │
118
+ │ ├── docs/
119
+ │ │ ├── form-builder-swagger.config.ts
120
+ │ │ └── index.ts
121
+ │ │
122
+ │ └── index.ts # Public API
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Module Setup
128
+
129
+ ### Basic Setup (Single Tenant)
130
+
131
+ ```typescript
132
+ import { FormBuilderModule } from '@flusys/nestjs-form-builder';
133
+
134
+ @Module({
135
+ imports: [
136
+ FormBuilderModule.forRoot({
137
+ global: true,
138
+ includeController: true,
139
+ bootstrapAppConfig: {
140
+ databaseMode: 'single',
141
+ enableCompanyFeature: false,
142
+ },
143
+ config: {
144
+ defaultDatabaseConfig: {
145
+ host: 'localhost',
146
+ port: 5432,
147
+ username: 'postgres',
148
+ password: 'password',
149
+ database: 'flusys',
150
+ },
151
+ },
152
+ }),
153
+ ],
154
+ })
155
+ export class AppModule {}
156
+ ```
157
+
158
+ ### With Company Feature
159
+
160
+ ```typescript
161
+ FormBuilderModule.forRoot({
162
+ bootstrapAppConfig: {
163
+ databaseMode: 'single',
164
+ enableCompanyFeature: true, // Enable company isolation
165
+ },
166
+ config: {
167
+ defaultDatabaseConfig: dbConfig,
168
+ },
169
+ })
170
+ ```
171
+
172
+ ### Async Configuration
173
+
174
+ ```typescript
175
+ FormBuilderModule.forRootAsync({
176
+ bootstrapAppConfig: {
177
+ databaseMode: 'single',
178
+ enableCompanyFeature: true,
179
+ },
180
+ useFactory: async (configService: ConfigService) => ({
181
+ defaultDatabaseConfig: configService.getDatabaseConfig(),
182
+ }),
183
+ inject: [ConfigService],
184
+ })
185
+ ```
186
+
187
+ ### Migration Configuration
188
+
189
+ Add form builder entities to your migration config:
190
+
191
+ ```typescript
192
+ import { getFormBuilderEntitiesByConfig } from '@flusys/nestjs-form-builder/entities';
193
+
194
+ function getEntitiesForTenant(tenantConfig?: ITenantDatabaseConfig): any[] {
195
+ const enableCompany = tenantConfig?.enableCompanyFeature ?? false;
196
+
197
+ // ... other entities
198
+ const formBuilderEntities = getFormBuilderEntitiesByConfig(enableCompany);
199
+
200
+ return [...otherEntities, ...formBuilderEntities];
201
+ }
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Entities
207
+
208
+ ### Entity Groups
209
+
210
+ ```typescript
211
+ // Core entities (no company feature)
212
+ export const FormCoreEntities = [Form, FormResult];
213
+
214
+ // Company-specific entities
215
+ export const FormCompanyEntities = [FormWithCompany, FormResult];
216
+
217
+ // Helper function
218
+ export function getFormBuilderEntitiesByConfig(enableCompanyFeature: boolean): any[] {
219
+ return enableCompanyFeature ? FormCompanyEntities : FormCoreEntities;
220
+ }
221
+
222
+ // Base type alias for backwards compatibility
223
+ export { Form as FormBase } from './form.entity';
224
+ ```
225
+
226
+ ### Form
227
+
228
+ Main form entity with all form fields:
229
+
230
+ | Column | Type | Description |
231
+ |--------|------|-------------|
232
+ | `name` | `varchar(255)` | Form name |
233
+ | `description` | `varchar(500)` | Optional description |
234
+ | `slug` | `varchar(255)` | URL-friendly identifier (unique) |
235
+ | `schema` | `json` | Form schema (sections, fields, settings) |
236
+ | `schemaVersion` | `int` | Auto-incremented on schema changes |
237
+ | `accessType` | `enum` | `public`, `authenticated`, `action_group` |
238
+ | `actionGroups` | `simple-array` | Permission codes for action_group access |
239
+ | `isActive` | `boolean` | Form availability |
240
+ | `metadata` | `simple-json` | Additional data |
241
+
242
+ ### Form vs FormWithCompany
243
+
244
+ - **Form** - Used when `enableCompanyFeature: false`
245
+ - **FormWithCompany** - Extends Form, adds `companyId` column for tenant isolation
246
+
247
+ ### FormResult
248
+
249
+ Stores form submissions:
250
+
251
+ | Column | Type | Description |
252
+ |--------|------|-------------|
253
+ | `formId` | `uuid` | Reference to form |
254
+ | `schemaVersionSnapshot` | `json` | Full schema copy at submission time |
255
+ | `schemaVersion` | `int` | Schema version at submission |
256
+ | `data` | `json` | Submitted field values |
257
+ | `submittedById` | `uuid` | User who submitted (null for public) |
258
+ | `submittedAt` | `timestamp` | Submission timestamp |
259
+ | `isDraft` | `boolean` | Draft vs final submission |
260
+ | `metadata` | `simple-json` | Additional data |
261
+
262
+ **Note:** FormResult doesn't have `companyId` - company context is derived from the linked Form via `formId`.
263
+
264
+ ---
265
+
266
+ ## DTOs
267
+
268
+ ### Form DTOs
269
+
270
+ | DTO | Purpose |
271
+ |-----|---------|
272
+ | `CreateFormDto` | Create new form |
273
+ | `UpdateFormDto` | Update existing form |
274
+ | `FormResponseDto` | Full form response |
275
+ | `PublicFormResponseDto` | Limited fields for public access |
276
+ | `FormAccessInfoResponseDto` | Access requirements info |
277
+
278
+ ### Form Result DTOs
279
+
280
+ | DTO | Purpose |
281
+ |-----|---------|
282
+ | `SubmitFormDto` | Public submission input |
283
+ | `CreateFormResultDto` | Internal with extra fields |
284
+ | `UpdateFormResultDto` | Update result |
285
+ | `GetMyDraftDto` | Get user's draft for a form |
286
+ | `UpdateDraftDto` | Update existing draft |
287
+ | `GetResultsByFormDto` | Query results by form ID |
288
+ | `FormResultResponseDto` | Result response |
289
+
290
+ ---
291
+
292
+ ## Services
293
+
294
+ ### FormBuilderConfigService
295
+
296
+ Provides access to module configuration:
297
+
298
+ ```typescript
299
+ @Injectable()
300
+ export class FormBuilderConfigService {
301
+ isCompanyFeatureEnabled(): boolean;
302
+ getDatabaseMode(): string;
303
+ isMultiTenant(): boolean;
304
+ getOptions(): FormBuilderModuleOptions;
305
+ getConfig();
306
+ }
307
+ ```
308
+
309
+ ### FormService
310
+
311
+ Extends `RequestScopedApiService` with form-specific operations:
312
+
313
+ ```typescript
314
+ // Get form for public submission
315
+ const form = await formService.getPublicForm(formId);
316
+
317
+ // Get form for authenticated submission (validates access)
318
+ const form = await formService.getAuthenticatedForm(formId, user);
319
+
320
+ // Get form by slug
321
+ const form = await formService.getBySlug('customer-feedback');
322
+
323
+ // Get access info (for frontend routing)
324
+ const info = await formService.getFormAccessInfo(formId);
325
+ ```
326
+
327
+ **Schema Versioning:**
328
+ - `schemaVersion` auto-increments when schema JSON changes
329
+ - Comparison uses `JSON.stringify` for deep equality check
330
+
331
+ ### FormResultService
332
+
333
+ Handles form submissions and drafts:
334
+
335
+ ```typescript
336
+ // Submit form (validates access type)
337
+ const result = await formResultService.submitForm(dto, user, isPublic);
338
+
339
+ // Get results by form with pagination
340
+ const { data, total } = await formResultService.getByFormId(formId, user, {
341
+ page: 0,
342
+ pageSize: 10,
343
+ });
344
+
345
+ // Draft management
346
+ const draft = await formResultService.getMyDraft(formId, user);
347
+ const updated = await formResultService.updateDraft(draftId, dto, user);
348
+ const hasSubmitted = await formResultService.hasUserSubmitted(formId, user);
349
+ ```
350
+
351
+ **Key behaviors:**
352
+ - Schema snapshot stored with each submission for historical accuracy
353
+ - Drafts auto-update if user re-submits as draft
354
+ - Final submission deletes existing draft
355
+ - Computed fields applied only on final submission (not drafts)
356
+
357
+ ---
358
+
359
+ ## Controllers
360
+
361
+ ### FormController
362
+
363
+ Base path: `/form-builder/form`
364
+
365
+ | Endpoint | Auth | Description |
366
+ |----------|------|-------------|
367
+ | `POST /insert` | JWT | Create form |
368
+ | `POST /update` | JWT | Update form |
369
+ | `POST /delete` | JWT | Delete form |
370
+ | `POST /get-all` | JWT | List forms |
371
+ | `POST /get/:id` | JWT | Get form by ID |
372
+ | `POST /access-info/:id` | Public | Get access requirements |
373
+ | `POST /public/:id` | Public | Get public form |
374
+ | `POST /authenticated/:id` | JWT | Get authenticated form |
375
+ | `POST /by-slug/:slug` | JWT | Get form by slug |
376
+ | `POST /public/by-slug/:slug` | Public | Get public form by slug |
377
+
378
+ ### FormResultController
379
+
380
+ Base path: `/form-builder/result`
381
+
382
+ | Endpoint | Auth | Description |
383
+ |----------|------|-------------|
384
+ | `POST /insert` | JWT | Create result (internal) |
385
+ | `POST /update` | JWT | Update result |
386
+ | `POST /delete` | JWT | Delete result |
387
+ | `POST /get-all` | JWT | List results |
388
+ | `POST /get/:id` | JWT | Get result by ID |
389
+ | `POST /submit` | JWT | Submit form (authenticated) |
390
+ | `POST /submit-public` | Public | Submit form (public) |
391
+ | `POST /my-draft` | JWT | Get user's draft for a form |
392
+ | `POST /update-draft` | JWT | Update draft or convert to final |
393
+ | `POST /by-form` | JWT | Get results by form ID |
394
+ | `POST /has-submitted` | JWT | Check if user has submitted |
395
+
396
+ ---
397
+
398
+ ## Access Control
399
+
400
+ ### Access Types
401
+
402
+ | Type | Description | Endpoint |
403
+ |------|-------------|----------|
404
+ | `public` | No authentication | `submit-public` |
405
+ | `authenticated` | Login required | `submit` |
406
+ | `action_group` | Specific permissions | `submit` + permission check |
407
+
408
+ ### Flow
409
+
410
+ 1. Frontend calls `access-info/:id` to determine requirements
411
+ 2. Based on `accessType`:
412
+ - `public` → Fetch via `public/:id`, submit via `submit-public`
413
+ - `authenticated` → Redirect to login if needed, use `authenticated/:id`, submit via `submit`
414
+ - `action_group` → Same as authenticated + permission check on submit
415
+
416
+ ### Permission Checking
417
+
418
+ For `action_group` forms:
419
+ - Form stores required permissions in `actionGroups` array
420
+ - Service checks if user has ANY of the listed permissions
421
+ - Uses cache-based permission lookup via `validateUserPermissions`
422
+
423
+ ```typescript
424
+ import { validateUserPermissions } from '@flusys/nestjs-form-builder';
425
+
426
+ // In FormService.getAuthenticatedForm()
427
+ if (form.accessType === FormAccessType.ACTION_GROUP && form.actionGroups?.length) {
428
+ const hasPermission = await validateUserPermissions(
429
+ user,
430
+ form.actionGroups,
431
+ this.cacheManager,
432
+ this.formBuilderConfig.isCompanyFeatureEnabled(),
433
+ this.logger,
434
+ 'accessing form',
435
+ form.id,
436
+ );
437
+ if (!hasPermission) {
438
+ throw new ForbiddenException('You do not have permission to access this form');
439
+ }
440
+ }
441
+ ```
442
+
443
+ ---
444
+
445
+ ## Multi-Tenant Support
446
+
447
+ ### Configuration
448
+
449
+ ```typescript
450
+ FormBuilderModule.forRoot({
451
+ bootstrapAppConfig: {
452
+ enableCompanyFeature: true,
453
+ },
454
+ })
455
+ ```
456
+
457
+ ### Entity Selection
458
+
459
+ The module automatically selects the correct entity:
460
+ - `enableCompanyFeature: false` → `Form` + `FormResult`
461
+ - `enableCompanyFeature: true` → `FormWithCompany` + `FormResult`
462
+
463
+ ### Company Filtering
464
+
465
+ When company feature is enabled:
466
+ - Forms are filtered by `user.companyId`
467
+ - Results are filtered via JOIN to Form's `companyId`
468
+ - New forms get `companyId` from user context or DTO
469
+
470
+ ### DataSource Provider
471
+
472
+ `FormBuilderDataSourceProvider` extends `MultiTenantDataSourceService`:
473
+ - Maintains separate static cache from other modules
474
+ - Dynamically loads correct entities per tenant
475
+ - Supports per-tenant feature flags
476
+
477
+ ---
478
+
479
+ ## API Reference
480
+
481
+ ### Module Options
482
+
483
+ ```typescript
484
+ interface FormBuilderModuleOptions {
485
+ global?: boolean; // Make module global
486
+ includeController?: boolean; // Include REST controllers
487
+ bootstrapAppConfig?: {
488
+ databaseMode?: 'single' | 'multi-tenant';
489
+ enableCompanyFeature?: boolean;
490
+ };
491
+ config?: {
492
+ defaultDatabaseConfig?: IDatabaseConfig;
493
+ };
494
+ }
495
+ ```
496
+
497
+ ### Interfaces
498
+
499
+ ```typescript
500
+ interface IForm {
501
+ id: string;
502
+ name: string;
503
+ description: string | null;
504
+ slug: string | null;
505
+ schema: Record<string, unknown>;
506
+ schemaVersion: number;
507
+ accessType: FormAccessType;
508
+ actionGroups: string[] | null;
509
+ isActive: boolean;
510
+ companyId: string | null;
511
+ metadata: Record<string, unknown> | null;
512
+ createdAt: Date;
513
+ updatedAt: Date;
514
+ deletedAt: Date | null;
515
+ createdById: string | null;
516
+ updatedById: string | null;
517
+ deletedById: string | null;
518
+ }
519
+
520
+ interface IFormResult {
521
+ id: string;
522
+ formId: string;
523
+ schemaVersionSnapshot: Record<string, unknown>;
524
+ schemaVersion: number;
525
+ data: Record<string, unknown>;
526
+ submittedById: string | null;
527
+ submittedAt: Date;
528
+ isDraft: boolean;
529
+ metadata: Record<string, unknown> | null;
530
+ // ... audit fields
531
+ }
532
+
533
+ interface IPublicForm {
534
+ id: string;
535
+ name: string;
536
+ description: string | null;
537
+ schema: Record<string, unknown>;
538
+ schemaVersion: number;
539
+ }
540
+ ```
541
+
542
+ ### Enums
543
+
544
+ ```typescript
545
+ enum FormAccessType {
546
+ PUBLIC = 'public',
547
+ AUTHENTICATED = 'authenticated',
548
+ ACTION_GROUP = 'action_group',
549
+ }
550
+ ```
551
+
552
+ ---
553
+
554
+ ## Computed Fields
555
+
556
+ Computed fields are values automatically calculated from form responses when a form is submitted. The calculation happens server-side before storing the result.
557
+
558
+ ### How It Works
559
+
560
+ 1. Form schema contains `computedFields` in settings
561
+ 2. On final submission (not drafts), backend calculates values
562
+ 3. Computed values are stored in `data._computed` namespace
563
+ 4. Original field values remain unchanged
564
+
565
+ ### Utility Functions
566
+
567
+ ```typescript
568
+ import { calculateComputedFields, IComputedField } from '@flusys/nestjs-form-builder';
569
+
570
+ // Calculate computed fields from form data
571
+ const computedValues = calculateComputedFields(formData, computedFields);
572
+ // Result: { total_score: 15, category: 'premium' }
573
+ ```
574
+
575
+ **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.
576
+
577
+ ### Supported Operations
578
+
579
+ | Type | Description |
580
+ |------|-------------|
581
+ | `direct` | Set a static value |
582
+ | `field_reference` | Copy value from another field |
583
+ | `arithmetic` | Calculate using sum, subtract, multiply, divide, average, min, max |
584
+
585
+ ### Condition Operators
586
+
587
+ Computed fields support conditional rules with these comparison operators:
588
+
589
+ | Operator | Description |
590
+ |----------|-------------|
591
+ | `equals`, `not_equals` | Value equality |
592
+ | `contains`, `not_contains` | String/array contains |
593
+ | `greater_than`, `less_than` | Numeric comparison |
594
+ | `greater_or_equal`, `less_or_equal` | Numeric comparison |
595
+ | `is_empty`, `is_not_empty` | Null/empty check |
596
+ | `is_before`, `is_after` | Date comparison |
597
+ | `is_checked`, `is_not_checked` | Boolean/checkbox |
598
+ | `is_any_of`, `is_none_of` | Array membership |
599
+
600
+ ### Data Storage
601
+
602
+ Submission data includes computed values:
603
+
604
+ ```json
605
+ {
606
+ "formId": "uuid",
607
+ "data": {
608
+ "name": "John",
609
+ "rating": 5,
610
+ "_computed": {
611
+ "total_score": 100,
612
+ "satisfaction_level": "high"
613
+ }
614
+ }
615
+ }
616
+ ```
617
+
618
+ ### Integration in Service
619
+
620
+ The `FormResultService` automatically calculates computed fields on submission via a private helper:
621
+
622
+ ```typescript
623
+ // Private method in FormResultService
624
+ private applyComputedFields(
625
+ data: Record<string, unknown>,
626
+ form: Form,
627
+ isDraft: boolean,
628
+ ): Record<string, unknown> {
629
+ if (isDraft) return data;
630
+
631
+ const schema = form.schema as Record<string, unknown>;
632
+ const settings = schema?.settings as Record<string, unknown> | undefined;
633
+ const computedFields = settings?.computedFields as IComputedField[] | undefined;
634
+
635
+ if (!computedFields || computedFields.length === 0) return data;
636
+
637
+ const computedValues = calculateComputedFields(data, computedFields);
638
+ return { ...data, _computed: computedValues };
639
+ }
640
+
641
+ // Used in submitForm() and updateDraft()
642
+ const finalData = this.applyComputedFields(dto.data, form, isDraft);
643
+ ```
644
+
645
+ ---
646
+
647
+ ## Response Mode
648
+
649
+ The form schema supports a `responseMode` setting that controls whether users can submit multiple responses.
650
+
651
+ ### Settings
652
+
653
+ | Mode | Description |
654
+ |------|-------------|
655
+ | `multiple` | Default. Users can submit unlimited responses |
656
+ | `single` | Each user can only submit once |
657
+
658
+ ### Tracking by Access Type
659
+
660
+ | Access Type | Tracking Method | Reliability |
661
+ |-------------|-----------------|-------------|
662
+ | `authenticated` | Server-side via `submittedById` | Reliable |
663
+ | `action_group` | Server-side via `submittedById` | Reliable |
664
+ | `public` | Client-side (frontend handles) | Best-effort only |
665
+
666
+ ### Backend Endpoint
667
+
668
+ For authenticated forms, the frontend calls `hasUserSubmitted` to check if the user has already submitted:
669
+
670
+ ```typescript
671
+ // FormResultController - POST /form-builder/result/has-submitted
672
+ @Post('has-submitted')
673
+ @UseGuards(JwtAuthGuard)
674
+ async hasUserSubmitted(
675
+ @Body() dto: GetMyDraftDto, // { formId: string }
676
+ @CurrentUser() user: ILoggedUserInfo,
677
+ ): Promise<boolean> {
678
+ return this.formResultService.hasUserSubmitted(dto.formId, user);
679
+ }
680
+ ```
681
+
682
+ **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.
683
+
684
+ ---
685
+
686
+ ## Best Practices
687
+
688
+ ### Schema Design
689
+
690
+ - Store complete form schema including sections, fields, and settings
691
+ - Use schema versioning to track changes
692
+ - Store schema snapshots with results for historical accuracy
693
+
694
+ ### Access Control
695
+
696
+ - Use `public` sparingly - only for truly anonymous forms
697
+ - Prefer `authenticated` for most internal forms
698
+ - Use `action_group` for sensitive forms requiring specific permissions
699
+
700
+ ### Company Isolation
701
+
702
+ - Always set `companyId` when company feature is enabled
703
+ - Use user's company context as default
704
+ - Allow explicit `companyId` in DTO for admin operations
705
+
706
+ ### Performance
707
+
708
+ - Use pagination when fetching results
709
+ - Select only needed fields in queries
710
+ - Consider caching frequently accessed forms
711
+
712
+ ---
713
+
714
+ ## See Also
715
+
716
+ - [ng-form-builder Guide](../../FLUSYS_NG/docs/FORM-BUILDER-GUIDE.md) - Frontend components
717
+ - [Shared Guide](SHARED-GUIDE.md) - Base classes and utilities
718
+ - [Auth Guide](AUTH-GUIDE.md) - User and company management
719
+
720
+ ---
721
+
722
+ **Last Updated:** 2026-02-21