@flusys/nestjs-form-builder 3.0.0-rc → 3.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
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Form Builder Package Guide
|
|
2
2
|
|
|
3
3
|
> **Package:** `@flusys/nestjs-form-builder`
|
|
4
|
+
> **Version:** 3.0.0
|
|
4
5
|
> **Type:** Dynamic form management with schema versioning and access control
|
|
5
6
|
|
|
6
7
|
This guide covers the NestJS form builder package - dynamic form creation, submission storage, and multi-tenant support.
|
|
@@ -22,6 +23,9 @@ This guide covers the NestJS form builder package - dynamic form creation, submi
|
|
|
22
23
|
- [Computed Fields](#computed-fields)
|
|
23
24
|
- [Response Mode](#response-mode)
|
|
24
25
|
- [Best Practices](#best-practices)
|
|
26
|
+
- [Swagger Configuration](#swagger-configuration)
|
|
27
|
+
- [Permission Utilities](#permission-utilities)
|
|
28
|
+
- [Controller Security](#controller-security)
|
|
25
29
|
|
|
26
30
|
---
|
|
27
31
|
|
|
@@ -227,39 +231,57 @@ export { Form as FormBase } from './form.entity';
|
|
|
227
231
|
|
|
228
232
|
Main form entity with all form fields:
|
|
229
233
|
|
|
230
|
-
| Column | Type | Description |
|
|
231
|
-
|
|
232
|
-
| `name` | `varchar(255)` | Form name |
|
|
233
|
-
| `description` | `varchar(500)` | Optional description |
|
|
234
|
-
| `slug` | `varchar(255)` | URL-friendly identifier
|
|
235
|
-
| `schema` | `json` | Form schema (sections, fields, settings) |
|
|
236
|
-
| `schemaVersion` | `int` | Auto-incremented on schema changes |
|
|
237
|
-
| `accessType` | `
|
|
238
|
-
| `actionGroups` | `simple-array` | Permission codes for action_group access |
|
|
239
|
-
| `isActive` | `boolean` | Form availability |
|
|
240
|
-
| `metadata` | `simple-json` | Additional data |
|
|
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
|
|
241
249
|
|
|
242
250
|
### Form vs FormWithCompany
|
|
243
251
|
|
|
244
252
|
- **Form** - Used when `enableCompanyFeature: false`
|
|
245
253
|
- **FormWithCompany** - Extends Form, adds `companyId` column for tenant isolation
|
|
246
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
|
+
|
|
247
266
|
### FormResult
|
|
248
267
|
|
|
249
268
|
Stores form submissions:
|
|
250
269
|
|
|
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 |
|
|
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
|
|
261
283
|
|
|
262
|
-
**Note:** FormResult doesn't have `companyId` - company context is derived from the linked Form via `formId`.
|
|
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.
|
|
263
285
|
|
|
264
286
|
---
|
|
265
287
|
|
|
@@ -275,6 +297,31 @@ Stores form submissions:
|
|
|
275
297
|
| `PublicFormResponseDto` | Limited fields for public access |
|
|
276
298
|
| `FormAccessInfoResponseDto` | Access requirements info |
|
|
277
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
|
+
|
|
278
325
|
### Form Result DTOs
|
|
279
326
|
|
|
280
327
|
| DTO | Purpose |
|
|
@@ -287,6 +334,27 @@ Stores form submissions:
|
|
|
287
334
|
| `GetResultsByFormDto` | Query results by form ID |
|
|
288
335
|
| `FormResultResponseDto` | Result response |
|
|
289
336
|
|
|
337
|
+
#### SubmitFormDto Fields
|
|
338
|
+
|
|
339
|
+
```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>;
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
#### GetResultsByFormDto Fields
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
class GetResultsByFormDto {
|
|
352
|
+
formId: string; // Required
|
|
353
|
+
page?: number; // Default: 0
|
|
354
|
+
pageSize?: number; // Default: 10
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
290
358
|
---
|
|
291
359
|
|
|
292
360
|
## Services
|
|
@@ -297,12 +365,21 @@ Provides access to module configuration:
|
|
|
297
365
|
|
|
298
366
|
```typescript
|
|
299
367
|
@Injectable()
|
|
300
|
-
export class FormBuilderConfigService {
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|
303
376
|
isMultiTenant(): boolean;
|
|
377
|
+
|
|
378
|
+
// Get full module options
|
|
304
379
|
getOptions(): FormBuilderModuleOptions;
|
|
305
|
-
|
|
380
|
+
|
|
381
|
+
// Get config section (defaultDatabaseConfig, tenants, etc.)
|
|
382
|
+
getConfig(): IFormBuilderConfig | undefined;
|
|
306
383
|
}
|
|
307
384
|
```
|
|
308
385
|
|
|
@@ -311,48 +388,111 @@ export class FormBuilderConfigService {
|
|
|
311
388
|
Extends `RequestScopedApiService` with form-specific operations:
|
|
312
389
|
|
|
313
390
|
```typescript
|
|
314
|
-
|
|
315
|
-
|
|
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 ===
|
|
316
408
|
|
|
317
|
-
// Get form for authenticated submission (validates access)
|
|
318
|
-
|
|
409
|
+
// Get form for authenticated submission (validates access + permissions)
|
|
410
|
+
async getAuthenticatedForm(formId: string, user: ILoggedUserInfo): Promise<IPublicForm>;
|
|
319
411
|
|
|
320
|
-
// Get form by slug
|
|
321
|
-
|
|
412
|
+
// Get form by slug (requires auth)
|
|
413
|
+
async getBySlug(slug: string): Promise<IForm | null>;
|
|
322
414
|
|
|
323
|
-
// Get
|
|
324
|
-
|
|
415
|
+
// Get form for submission (internal - validates access type)
|
|
416
|
+
async getFormForSubmission(formId: string, user: ILoggedUserInfo | null): Promise<Form>;
|
|
417
|
+
}
|
|
325
418
|
```
|
|
326
419
|
|
|
327
420
|
**Schema Versioning:**
|
|
328
421
|
- `schemaVersion` auto-increments when schema JSON changes
|
|
329
422
|
- Comparison uses `JSON.stringify` for deep equality check
|
|
423
|
+
- Version tracked in FormResult snapshots for historical accuracy
|
|
330
424
|
|
|
331
425
|
### FormResultService
|
|
332
426
|
|
|
333
427
|
Handles form submissions and drafts:
|
|
334
428
|
|
|
335
429
|
```typescript
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
}
|
|
349
468
|
```
|
|
350
469
|
|
|
351
470
|
**Key behaviors:**
|
|
352
471
|
- Schema snapshot stored with each submission for historical accuracy
|
|
353
472
|
- Drafts auto-update if user re-submits as draft
|
|
354
|
-
- Final submission deletes existing draft
|
|
473
|
+
- Final submission deletes existing draft (soft delete)
|
|
355
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
|
+
```
|
|
356
496
|
|
|
357
497
|
---
|
|
358
498
|
|
|
@@ -481,16 +621,37 @@ When company feature is enabled:
|
|
|
481
621
|
### Module Options
|
|
482
622
|
|
|
483
623
|
```typescript
|
|
484
|
-
interface FormBuilderModuleOptions {
|
|
485
|
-
global?: boolean;
|
|
486
|
-
includeController?: boolean;
|
|
487
|
-
bootstrapAppConfig?:
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
+
}
|
|
652
|
+
|
|
653
|
+
interface FormBuilderOptionsFactory {
|
|
654
|
+
createFormBuilderOptions(): Promise<IFormBuilderConfig> | IFormBuilderConfig;
|
|
494
655
|
}
|
|
495
656
|
```
|
|
496
657
|
|
|
@@ -574,28 +735,119 @@ const computedValues = calculateComputedFields(formData, computedFields);
|
|
|
574
735
|
|
|
575
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.
|
|
576
737
|
|
|
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
|
+
}
|
|
750
|
+
|
|
751
|
+
interface IComputedRule {
|
|
752
|
+
id: string;
|
|
753
|
+
condition?: IComputedConditionGroup; // Optional - no condition = always apply
|
|
754
|
+
computation: IComputation;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
interface IComputedConditionGroup {
|
|
758
|
+
operator: 'AND' | 'OR';
|
|
759
|
+
conditions: IComputedCondition[];
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
interface IComputedCondition {
|
|
763
|
+
fieldId: string;
|
|
764
|
+
comparison: string; // See Condition Operators below
|
|
765
|
+
value: unknown;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
interface IComputation {
|
|
769
|
+
type: ComputationType;
|
|
770
|
+
config: IDirectValueConfig | IFieldReferenceConfig | IArithmeticConfig;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
type ComputationType = 'direct' | 'field_reference' | 'arithmetic';
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
### Computation Config Types
|
|
777
|
+
|
|
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
|
+
}
|
|
797
|
+
|
|
798
|
+
interface IArithmeticOperand {
|
|
799
|
+
type: 'field' | 'constant';
|
|
800
|
+
fieldId?: string; // When type = 'field'
|
|
801
|
+
value?: number; // When type = 'constant'
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
577
805
|
### Supported Operations
|
|
578
806
|
|
|
579
807
|
| Type | Description |
|
|
580
808
|
|------|-------------|
|
|
581
809
|
| `direct` | Set a static value |
|
|
582
810
|
| `field_reference` | Copy value from another field |
|
|
583
|
-
| `arithmetic` | Calculate using
|
|
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 |
|
|
584
826
|
|
|
585
827
|
### Condition Operators
|
|
586
828
|
|
|
587
829
|
Computed fields support conditional rules with these comparison operators:
|
|
588
830
|
|
|
589
|
-
| Operator | Description |
|
|
590
|
-
|
|
591
|
-
| `equals
|
|
592
|
-
| `
|
|
593
|
-
| `
|
|
594
|
-
| `
|
|
595
|
-
| `
|
|
596
|
-
| `
|
|
597
|
-
| `
|
|
598
|
-
| `
|
|
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 |
|
|
599
851
|
|
|
600
852
|
### Data Storage
|
|
601
853
|
|
|
@@ -711,6 +963,96 @@ async hasUserSubmitted(
|
|
|
711
963
|
|
|
712
964
|
---
|
|
713
965
|
|
|
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
|
|
985
|
+
|
|
986
|
+
When `enableCompanyFeature: false`, these schemas hide `companyId`:
|
|
987
|
+
- `CreateFormDto`
|
|
988
|
+
- `UpdateFormDto`
|
|
989
|
+
- `FormQueryDto`
|
|
990
|
+
- `FormResponseDto`
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
## Permission Utilities
|
|
995
|
+
|
|
996
|
+
The package provides permission validation utilities for action group access:
|
|
997
|
+
|
|
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
|
|
1017
|
+
|
|
1018
|
+
### Permission Cache Key Format
|
|
1019
|
+
|
|
1020
|
+
```typescript
|
|
1021
|
+
// With company feature enabled
|
|
1022
|
+
`permissions:company:${companyId}:branch:${branchId}:user:${userId}`
|
|
1023
|
+
|
|
1024
|
+
// Without company feature
|
|
1025
|
+
`permissions:user:${userId}`
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
---
|
|
1029
|
+
|
|
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
|
+
|
|
714
1056
|
## See Also
|
|
715
1057
|
|
|
716
1058
|
- [ng-form-builder Guide](../../FLUSYS_NG/docs/FORM-BUILDER-GUIDE.md) - Frontend components
|
|
@@ -719,4 +1061,4 @@ async hasUserSubmitted(
|
|
|
719
1061
|
|
|
720
1062
|
---
|
|
721
1063
|
|
|
722
|
-
**Last Updated:** 2026-02-
|
|
1064
|
+
**Last Updated:** 2026-02-25
|
|
@@ -92,14 +92,8 @@ let FormBuilderDataSourceProvider = class FormBuilderDataSourceProvider extends
|
|
|
92
92
|
}
|
|
93
93
|
async getFormBuilderEntities(enableCompanyFeature) {
|
|
94
94
|
const enable = enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
|
|
95
|
-
const {
|
|
96
|
-
return enable
|
|
97
|
-
FormWithCompany,
|
|
98
|
-
FormResult
|
|
99
|
-
] : [
|
|
100
|
-
Form,
|
|
101
|
-
FormResult
|
|
102
|
-
];
|
|
95
|
+
const { getFormBuilderEntitiesByConfig } = await Promise.resolve().then(()=>/*#__PURE__*/ _interop_require_wildcard(require("../entities")));
|
|
96
|
+
return getFormBuilderEntitiesByConfig(enable);
|
|
103
97
|
}
|
|
104
98
|
async createDataSourceFromConfig(config) {
|
|
105
99
|
const enableCompanyFeature = this.configService.isCompanyFeatureEnabled(this.getCurrentTenant() ?? undefined);
|
|
@@ -41,14 +41,8 @@ export class FormBuilderDataSourceProvider extends MultiTenantDataSourceService
|
|
|
41
41
|
}
|
|
42
42
|
async getFormBuilderEntities(enableCompanyFeature) {
|
|
43
43
|
const enable = enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
|
|
44
|
-
const {
|
|
45
|
-
return enable
|
|
46
|
-
FormWithCompany,
|
|
47
|
-
FormResult
|
|
48
|
-
] : [
|
|
49
|
-
Form,
|
|
50
|
-
FormResult
|
|
51
|
-
];
|
|
44
|
+
const { getFormBuilderEntitiesByConfig } = await import('../entities');
|
|
45
|
+
return getFormBuilderEntitiesByConfig(enable);
|
|
52
46
|
}
|
|
53
47
|
async createDataSourceFromConfig(config) {
|
|
54
48
|
const enableCompanyFeature = this.configService.isCompanyFeatureEnabled(this.getCurrentTenant() ?? undefined);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-form-builder",
|
|
3
|
-
"version": "3.0.0
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Dynamic form builder module with schema versioning and access control",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"typeorm": "^0.3.0"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@flusys/nestjs-core": "3.0.0
|
|
87
|
-
"@flusys/nestjs-shared": "3.0.0
|
|
86
|
+
"@flusys/nestjs-core": "3.0.0",
|
|
87
|
+
"@flusys/nestjs-shared": "3.0.0"
|
|
88
88
|
}
|
|
89
89
|
}
|