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