@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.
- package/README.md +722 -0
- package/cjs/controllers/form-result.controller.js +67 -5
- package/cjs/controllers/form.controller.js +48 -15
- package/cjs/docs/form-builder-swagger.config.js +6 -100
- package/cjs/dtos/form-result.dto.js +6 -93
- package/cjs/dtos/form.dto.js +21 -163
- package/cjs/entities/form-with-company.entity.js +12 -2
- package/cjs/entities/form.entity.js +103 -3
- package/cjs/entities/index.js +28 -16
- package/cjs/index.js +1 -0
- package/cjs/interfaces/form-result.interface.js +1 -6
- package/cjs/modules/form-builder.module.js +57 -83
- package/cjs/services/form-builder-config.service.js +6 -16
- package/cjs/services/form-builder-datasource.provider.js +19 -59
- package/cjs/services/form-result.service.js +107 -181
- package/cjs/services/form.service.js +56 -72
- package/cjs/utils/computed-field.utils.js +17 -29
- package/cjs/utils/permission.utils.js +11 -16
- package/controllers/form-result.controller.d.ts +10 -12
- package/dtos/form-result.dto.d.ts +2 -19
- package/dtos/form.dto.d.ts +6 -32
- package/entities/form-with-company.entity.d.ts +2 -2
- package/entities/form.entity.d.ts +12 -2
- package/entities/index.d.ts +7 -2
- package/fesm/controllers/form-result.controller.js +69 -7
- package/fesm/controllers/form.controller.js +50 -17
- package/fesm/docs/form-builder-swagger.config.js +6 -100
- package/fesm/dtos/form-result.dto.js +9 -99
- package/fesm/dtos/form.dto.js +22 -165
- package/fesm/entities/form-with-company.entity.js +12 -2
- package/fesm/entities/form.entity.js +104 -4
- package/fesm/entities/index.js +18 -24
- package/fesm/index.js +2 -0
- package/fesm/modules/form-builder.module.js +57 -83
- package/fesm/services/form-builder-config.service.js +6 -16
- package/fesm/services/form-builder-datasource.provider.js +19 -59
- package/fesm/services/form-result.service.js +107 -181
- package/fesm/services/form.service.js +56 -72
- package/fesm/utils/computed-field.utils.js +17 -29
- package/fesm/utils/permission.utils.js +2 -9
- package/index.d.ts +1 -0
- package/interfaces/form-builder-module.interface.d.ts +4 -7
- package/interfaces/form-result.interface.d.ts +2 -9
- package/interfaces/form.interface.d.ts +2 -10
- package/modules/form-builder.module.d.ts +4 -3
- package/package.json +3 -3
- package/services/form-builder-config.service.d.ts +5 -3
- package/services/form-builder-datasource.provider.d.ts +3 -6
- package/services/form-result.service.d.ts +5 -0
- package/services/form.service.d.ts +13 -10
- package/utils/permission.utils.d.ts +0 -2
- package/cjs/entities/form-base.entity.js +0 -113
- package/entities/form-base.entity.d.ts +0 -13
- 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
|