@flusys/nestjs-form-builder
v4.0.0-lts
Published
Dynamic form builder module with schema versioning and access control
Maintainers
Readme
Form Builder Package Guide
Package:
@flusys/nestjs-form-builderVersion: 3.0.1 Type: Dynamic form management with schema versioning and access control
This guide covers the NestJS form builder package - dynamic form creation, submission storage, and multi-tenant support.
Table of Contents
- Overview
- Installation
- Constants
- Package Architecture
- Module Setup
- Entities
- DTOs
- Services
- Controllers
- Access Control
- Multi-Tenant Support
- API Reference
- Computed Fields
- Response Mode
- Best Practices
- Swagger Configuration
- Permission Utilities
- Controller Security
Overview
@flusys/nestjs-form-builder provides a comprehensive form management system:
- Dynamic Forms - JSON schema-based form definitions
- Schema Versioning - Auto-increment version on schema changes
- Result Snapshots - Store schema at submission time for historical accuracy
- Access Control - Public, authenticated, and permission-based access
- Multi-Tenant Support - Optional company isolation
- POST-only RPC - Follows project API conventions
Package Hierarchy
@flusys/nestjs-core ← Foundation
↓
@flusys/nestjs-shared ← Shared utilities
↓
@flusys/nestjs-form-builder ← Form management (THIS PACKAGE)Installation
npm install @flusys/nestjs-form-builder @flusys/nestjs-shared @flusys/nestjs-coreConstants
// Injection Token
export const FORM_BUILDER_MODULE_OPTIONS = 'FORM_BUILDER_MODULE_OPTIONS';Package Architecture
nestjs-form-builder/
├── src/
│ ├── modules/
│ │ └── form-builder.module.ts # Main module
│ │
│ ├── config/
│ │ ├── form-builder.constants.ts # Constants
│ │ └── index.ts
│ │
│ ├── entities/
│ │ ├── form.entity.ts # Main form entity
│ │ ├── form-with-company.entity.ts # Extends Form with company
│ │ ├── form-result.entity.ts # Submissions
│ │ └── index.ts
│ │
│ ├── dtos/
│ │ ├── form.dto.ts # Form DTOs
│ │ ├── form-result.dto.ts # Result DTOs
│ │ └── index.ts
│ │
│ ├── services/
│ │ ├── form-builder-config.service.ts # Config service
│ │ ├── form-builder-datasource.provider.ts
│ │ ├── form.service.ts # Form CRUD
│ │ ├── form-result.service.ts # Submission handling
│ │ └── index.ts
│ │
│ ├── controllers/
│ │ ├── form.controller.ts # Form endpoints
│ │ ├── form-result.controller.ts # Result endpoints
│ │ └── index.ts
│ │
│ ├── enums/
│ │ ├── form-access-type.enum.ts
│ │ └── index.ts
│ │
│ ├── interfaces/
│ │ ├── form.interface.ts
│ │ ├── form-result.interface.ts
│ │ ├── form-builder-module.interface.ts
│ │ └── index.ts
│ │
│ ├── utils/
│ │ ├── permission.utils.ts # Permission validation
│ │ ├── computed-field.utils.ts # Computed field calculation
│ │ └── index.ts
│ │
│ ├── docs/
│ │ ├── form-builder-swagger.config.ts
│ │ └── index.ts
│ │
│ └── index.ts # Public APIModule Setup
Basic Setup (Single Tenant)
import { FormBuilderModule } from '@flusys/nestjs-form-builder';
@Module({
imports: [
FormBuilderModule.forRoot({
global: true,
includeController: true,
bootstrapAppConfig: {
databaseMode: 'single',
enableCompanyFeature: false,
},
config: {
defaultDatabaseConfig: {
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'flusys',
},
},
}),
],
})
export class AppModule {}With Company Feature
FormBuilderModule.forRoot({
bootstrapAppConfig: {
databaseMode: 'single',
enableCompanyFeature: true, // Enable company isolation
},
config: {
defaultDatabaseConfig: dbConfig,
},
})Async Configuration
FormBuilderModule.forRootAsync({
bootstrapAppConfig: {
databaseMode: 'single',
enableCompanyFeature: true,
},
useFactory: async (configService: ConfigService) => ({
defaultDatabaseConfig: configService.getDatabaseConfig(),
}),
inject: [ConfigService],
})Migration Configuration
Add form builder entities to your migration config:
import { getFormBuilderEntitiesByConfig } from '@flusys/nestjs-form-builder/entities';
function getEntitiesForTenant(tenantConfig?: ITenantDatabaseConfig): any[] {
const enableCompany = tenantConfig?.enableCompanyFeature ?? false;
// ... other entities
const formBuilderEntities = getFormBuilderEntitiesByConfig(enableCompany);
return [...otherEntities, ...formBuilderEntities];
}Entities
Entity Groups
// Core entities (no company feature)
export const FormCoreEntities = [Form, FormResult];
// Company-specific entities
export const FormCompanyEntities = [FormWithCompany, FormResult];
// Helper function
export function getFormBuilderEntitiesByConfig(enableCompanyFeature: boolean): any[] {
return enableCompanyFeature ? FormCompanyEntities : FormCoreEntities;
}
// Base type alias for backwards compatibility
export { Form as FormBase } from './form.entity';Form
Main form entity with all form fields:
| Column | Type | Default | Description |
|--------|------|---------|-------------|
| name | varchar(255) | required | Form name |
| description | varchar(500) | null | Optional description |
| slug | varchar(255) | null | URL-friendly identifier |
| schema | json | required | Form schema (sections, fields, settings) |
| schemaVersion | int | 1 | Auto-incremented on schema changes |
| accessType | varchar(50) | 'AUTHENTICATED' | public, authenticated, action_group |
| actionGroups | simple-array | null | Permission codes for action_group access |
| isActive | boolean | true | Form availability |
| metadata | simple-json | null | Additional data |
Indexes (Form):
slug- Unique indexisActive- Index for filtering active forms
Form vs FormWithCompany
- Form - Used when
enableCompanyFeature: false - FormWithCompany - Extends Form, adds
companyIdcolumn for tenant isolation
FormWithCompany Additional Column:
| Column | Type | Description |
|--------|------|-------------|
| companyId | uuid | Company ID for tenant isolation |
FormWithCompany Indexes:
companyId- Index for company filteringcompanyId, slug- Unique compound index (slugs unique per company)companyId, isActive- Compound index for active forms per company
FormResult
Stores form submissions:
| Column | Type | Default | Description |
|--------|------|---------|-------------|
| formId | uuid | required | Reference to form |
| schemaVersionSnapshot | json | required | Full schema copy at submission time |
| schemaVersion | int | required | Schema version at submission |
| data | json | required | Submitted field values |
| submittedById | uuid | null | User who submitted (null for public) |
| submittedAt | timestamp | required | Submission timestamp |
| isDraft | boolean | false | Draft vs final submission |
| metadata | simple-json | null | Additional data |
Indexes:
formId- Index for filtering by form
Note: FormResult doesn't have companyId - company context is derived from the linked Form via formId. Company filtering is applied via JOIN in queries.
DTOs
Form DTOs
| DTO | Purpose |
|-----|---------|
| CreateFormDto | Create new form |
| UpdateFormDto | Update existing form |
| FormResponseDto | Full form response |
| PublicFormResponseDto | Limited fields for public access |
| FormAccessInfoResponseDto | Access requirements info |
CreateFormDto Fields
class CreateFormDto {
name: string; // Required, max 255 chars
description?: string; // Optional, max 500 chars
slug?: string; // Optional, max 255 chars (unique)
schema: Record<string, unknown>; // Required - form structure
accessType?: FormAccessType; // Default: AUTHENTICATED
actionGroups?: string[]; // For ACTION_GROUP access type
companyId?: string; // When company feature enabled
isActive?: boolean; // Default: true
metadata?: Record<string, unknown>;
}UpdateFormDto Fields
class UpdateFormDto extends PartialType(CreateFormDto) {
id: string; // Required
schemaVersion?: number; // Auto-incremented on schema change
}Form Result DTOs
| DTO | Purpose |
|-----|---------|
| SubmitFormDto | Public submission input |
| CreateFormResultDto | Internal with extra fields |
| UpdateFormResultDto | Update result |
| GetMyDraftDto | Get user's draft for a form |
| UpdateDraftDto | Update existing draft |
| GetResultsByFormDto | Query results by form ID |
| FormResultResponseDto | Result response |
SubmitFormDto Fields
class SubmitFormDto {
formId: string; // Required
data: Record<string, unknown>; // Required - field values
isDraft?: boolean; // Default: false
metadata?: Record<string, unknown>;
}GetResultsByFormDto Fields
class GetResultsByFormDto {
formId: string; // Required
page?: number; // Default: 0
pageSize?: number; // Default: 10
}Services
FormBuilderConfigService
Provides access to module configuration:
@Injectable()
export class FormBuilderConfigService implements IModuleConfigService {
// Check if company feature is enabled (supports per-tenant override)
isCompanyFeatureEnabled(tenant?: ITenantDatabaseConfig): boolean;
// Get database mode ('single' | 'multi-tenant')
getDatabaseMode(): DatabaseMode;
// Check if running in multi-tenant mode
isMultiTenant(): boolean;
// Get full module options
getOptions(): FormBuilderModuleOptions;
// Get config section (defaultDatabaseConfig, tenants, etc.)
getConfig(): IFormBuilderConfig | undefined;
}FormService
Extends RequestScopedApiService with form-specific operations:
@Injectable({ scope: Scope.REQUEST })
export class FormService extends RequestScopedApiService<...> {
// === Standard CRUD (inherited) ===
// insert(dto, user), update(dto, user), delete(id, user), getAll(filter, user), getById(id, user)
// === Public Access (no auth required) ===
// Get form for public submission (accessType must be PUBLIC)
async getPublicForm(formId: string): Promise<IPublicForm>;
// Get public form by slug (no auth required)
async getPublicFormBySlug(slug: string): Promise<IPublicForm | null>;
// Get access info for routing decisions
async getFormAccessInfo(formId: string): Promise<FormAccessInfoResponseDto>;
// === Authenticated Access ===
// Get form for authenticated submission (validates access + permissions)
async getAuthenticatedForm(formId: string, user: ILoggedUserInfo): Promise<IPublicForm>;
// Get form by slug (requires auth)
async getBySlug(slug: string): Promise<IForm | null>;
// Get form for submission (internal - validates access type)
async getFormForSubmission(formId: string, user: ILoggedUserInfo | null): Promise<Form>;
}Schema Versioning:
schemaVersionauto-increments when schema JSON changes- Comparison uses
JSON.stringifyfor deep equality check - Version tracked in FormResult snapshots for historical accuracy
FormResultService
Handles form submissions and drafts:
@Injectable({ scope: Scope.REQUEST })
export class FormResultService extends RequestScopedApiService<...> {
// === Standard CRUD (inherited) ===
// insert(dto, user), update(dto, user), delete(id, user), getAll(filter, user), getById(id, user)
// === Form Submission ===
// Submit form (validates access type, handles drafts)
async submitForm(
dto: SubmitFormDto,
user: ILoggedUserInfo | null,
isPublic?: boolean, // default: false
): Promise<IFormResult>;
// === Draft Management ===
// Get user's draft for a specific form
async getMyDraft(formId: string, user: ILoggedUserInfo): Promise<IFormResult | null>;
// Update existing draft (can convert to final submission)
async updateDraft(
draftId: string,
dto: SubmitFormDto,
user: ILoggedUserInfo,
): Promise<IFormResult>;
// === Query Methods ===
// Get results by form ID with pagination
async getByFormId(
formId: string,
user: ILoggedUserInfo | null,
pagination?: { page?: number; pageSize?: number },
): Promise<{ data: IFormResult[]; total: number }>;
// Check if user has submitted (non-draft) for single response mode
async hasUserSubmitted(formId: string, user: ILoggedUserInfo): Promise<boolean>;
}Key behaviors:
- Schema snapshot stored with each submission for historical accuracy
- Drafts auto-update if user re-submits as draft
- Final submission deletes existing draft (soft delete)
- Computed fields applied only on final submission (not drafts)
- Company filtering via JOIN when company feature enabled
FormBuilderDataSourceProvider
Extends MultiTenantDataSourceService for dynamic entity loading:
@Injectable({ scope: Scope.REQUEST })
export class FormBuilderDataSourceProvider extends MultiTenantDataSourceService {
// Maintains separate static cache from other modules
protected static readonly tenantConnections = new Map<string, DataSource>();
protected static singleDataSource: DataSource | null = null;
// Get entities based on company feature flag
async getFormBuilderEntities(enableCompanyFeature?: boolean): Promise<any[]>;
// Inherited from MultiTenantDataSourceService
async getDataSource(): Promise<DataSource>;
async getRepository<T>(entity: EntityTarget<T>): Promise<Repository<T>>;
}Controllers
FormController
Base path: /form-builder/form
| Endpoint | Auth | Description |
|----------|------|-------------|
| POST /insert | JWT | Create form |
| POST /update | JWT | Update form |
| POST /delete | JWT | Delete form |
| POST /get-all | JWT | List forms |
| POST /get/:id | JWT | Get form by ID |
| POST /access-info/:id | Public | Get access requirements |
| POST /public/:id | Public | Get public form |
| POST /authenticated/:id | JWT | Get authenticated form |
| POST /by-slug/:slug | JWT | Get form by slug |
| POST /public/by-slug/:slug | Public | Get public form by slug |
FormResultController
Base path: /form-builder/result
| Endpoint | Auth | Description |
|----------|------|-------------|
| POST /insert | JWT | Create result (internal) |
| POST /update | JWT | Update result |
| POST /delete | JWT | Delete result |
| POST /get-all | JWT | List results |
| POST /get/:id | JWT | Get result by ID |
| POST /submit | JWT | Submit form (authenticated) |
| POST /submit-public | Public | Submit form (public) |
| POST /my-draft | JWT | Get user's draft for a form |
| POST /update-draft | JWT | Update draft or convert to final |
| POST /by-form | JWT | Get results by form ID |
| POST /has-submitted | JWT | Check if user has submitted |
Access Control
Access Types
| Type | Description | Endpoint |
|------|-------------|----------|
| public | No authentication | submit-public |
| authenticated | Login required | submit |
| action_group | Specific permissions | submit + permission check |
Flow
- Frontend calls
access-info/:idto determine requirements - Based on
accessType:public→ Fetch viapublic/:id, submit viasubmit-publicauthenticated→ Redirect to login if needed, useauthenticated/:id, submit viasubmitaction_group→ Same as authenticated + permission check on submit
Permission Checking
For action_group forms:
- Form stores required permissions in
actionGroupsarray - Service checks if user has ANY of the listed permissions
- Uses cache-based permission lookup via
validateUserPermissions
import { validateUserPermissions } from '@flusys/nestjs-form-builder';
// In FormService.getAuthenticatedForm()
if (form.accessType === FormAccessType.ACTION_GROUP && form.actionGroups?.length) {
const hasPermission = await validateUserPermissions(
user,
form.actionGroups,
this.cacheManager,
this.formBuilderConfig.isCompanyFeatureEnabled(),
this.logger,
'accessing form',
form.id,
);
if (!hasPermission) {
throw new ForbiddenException('You do not have permission to access this form');
}
}Multi-Tenant Support
Configuration
FormBuilderModule.forRoot({
bootstrapAppConfig: {
enableCompanyFeature: true,
},
})Entity Selection
The module automatically selects the correct entity:
enableCompanyFeature: false→Form+FormResultenableCompanyFeature: true→FormWithCompany+FormResult
Company Filtering
When company feature is enabled:
- Forms are filtered by
user.companyId - Results are filtered via JOIN to Form's
companyId - New forms get
companyIdfrom user context or DTO
DataSource Provider
FormBuilderDataSourceProvider extends MultiTenantDataSourceService:
- Maintains separate static cache from other modules
- Dynamically loads correct entities per tenant
- Supports per-tenant feature flags
API Reference
Module Options
interface FormBuilderModuleOptions extends IDynamicModuleConfig {
global?: boolean; // Make module global
includeController?: boolean; // Include REST controllers
bootstrapAppConfig?: IBootstrapAppConfig; // Bootstrap configuration
config?: IFormBuilderConfig; // Form builder configuration
}
interface IFormBuilderConfig extends IDataSourceServiceOptions {
// Currently no form-builder specific runtime config
// Add form-builder specific settings here as needed
defaultDatabaseConfig?: IDatabaseConfig;
tenantDefaultDatabaseConfig?: IDatabaseConfig;
tenants?: ITenantDatabaseConfig[];
}Async Options
interface FormBuilderModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
global?: boolean;
includeController?: boolean;
bootstrapAppConfig?: IBootstrapAppConfig;
useFactory?: (...args: any[]) => Promise<IFormBuilderConfig> | IFormBuilderConfig;
inject?: any[];
useExisting?: Type<FormBuilderOptionsFactory>;
useClass?: Type<FormBuilderOptionsFactory>;
}
interface FormBuilderOptionsFactory {
createFormBuilderOptions(): Promise<IFormBuilderConfig> | IFormBuilderConfig;
}Interfaces
interface IForm {
id: string;
name: string;
description: string | null;
slug: string | null;
schema: Record<string, unknown>;
schemaVersion: number;
accessType: FormAccessType;
actionGroups: string[] | null;
isActive: boolean;
companyId: string | null;
metadata: Record<string, unknown> | null;
createdAt: Date;
updatedAt: Date;
deletedAt: Date | null;
createdById: string | null;
updatedById: string | null;
deletedById: string | null;
}
interface IFormResult {
id: string;
formId: string;
schemaVersionSnapshot: Record<string, unknown>;
schemaVersion: number;
data: Record<string, unknown>;
submittedById: string | null;
submittedAt: Date;
isDraft: boolean;
metadata: Record<string, unknown> | null;
// ... audit fields
}
interface IPublicForm {
id: string;
name: string;
description: string | null;
schema: Record<string, unknown>;
schemaVersion: number;
}Enums
enum FormAccessType {
PUBLIC = 'public',
AUTHENTICATED = 'authenticated',
ACTION_GROUP = 'action_group',
}Computed Fields
Computed fields are values automatically calculated from form responses when a form is submitted. The calculation happens server-side before storing the result.
How It Works
- Form schema contains
computedFieldsin settings - On final submission (not drafts), backend calculates values
- Computed values are stored in
data._computednamespace - Original field values remain unchanged
Utility Functions
import { calculateComputedFields, IComputedField } from '@flusys/nestjs-form-builder';
// Calculate computed fields from form data
const computedValues = calculateComputedFields(formData, computedFields);
// Result: { total_score: 15, category: 'premium' }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.
Computed Field Interfaces
interface IComputedField {
id: string;
name: string;
key: string; // Storage key in _computed
valueType: 'string' | 'number';
rules: IComputedRule[];
defaultValue?: string | number | null;
description?: string;
}
interface IComputedRule {
id: string;
condition?: IComputedConditionGroup; // Optional - no condition = always apply
computation: IComputation;
}
interface IComputedConditionGroup {
operator: 'AND' | 'OR';
conditions: IComputedCondition[];
}
interface IComputedCondition {
fieldId: string;
comparison: string; // See Condition Operators below
value: unknown;
}
interface IComputation {
type: ComputationType;
config: IDirectValueConfig | IFieldReferenceConfig | IArithmeticConfig;
}
type ComputationType = 'direct' | 'field_reference' | 'arithmetic';Computation Config Types
// Direct value - set a static value
interface IDirectValueConfig {
type: 'direct';
value: string | number;
}
// Field reference - copy value from another field
interface IFieldReferenceConfig {
type: 'field_reference';
fieldId: string;
}
// Arithmetic - calculate from multiple operands
interface IArithmeticConfig {
type: 'arithmetic';
operation: ArithmeticOperation;
operands: IArithmeticOperand[];
}
interface IArithmeticOperand {
type: 'field' | 'constant';
fieldId?: string; // When type = 'field'
value?: number; // When type = 'constant'
}Supported Operations
| Type | Description |
|------|-------------|
| direct | Set a static value |
| field_reference | Copy value from another field |
| arithmetic | Calculate using arithmetic operations |
Arithmetic Operations
| Operation | Description |
|-----------|-------------|
| sum | Add all operand values |
| subtract | Subtract subsequent values from first |
| multiply | Multiply all operand values |
| divide | Divide first value by subsequent values |
| average | Calculate average of all operands |
| min | Get minimum value |
| max | Get maximum value |
| increment | Alias for sum |
| decrement | Alias for subtract |
Condition Operators
Computed fields support conditional rules with these comparison operators:
| Operator | Aliases | Description |
|----------|---------|-------------|
| equals | | Value equality (string-safe) |
| not_equals | | Value inequality |
| contains | | String contains substring |
| not_contains | | String does not contain substring |
| starts_with | | String starts with value |
| ends_with | | String ends with value |
| greater_than | | Numeric greater than |
| less_than | | Numeric less than |
| greater_or_equal | | Numeric greater or equal |
| less_or_equal | | Numeric less or equal |
| is_empty | | Null, undefined, empty string, or empty array |
| is_not_empty | | Has a non-empty value |
| is_before | | Date comparison (before) |
| is_after | | Date comparison (after) |
| is_checked | | Boolean true (or 'true', or 1) |
| is_not_checked | | Boolean false (or 'false', 0, falsy) |
| is_any_of | in | Value is in array |
| is_none_of | not_in | Value is not in array |
Data Storage
Submission data includes computed values:
{
"formId": "uuid",
"data": {
"name": "John",
"rating": 5,
"_computed": {
"total_score": 100,
"satisfaction_level": "high"
}
}
}Integration in Service
The FormResultService automatically calculates computed fields on submission via a private helper:
// Private method in FormResultService
private applyComputedFields(
data: Record<string, unknown>,
form: Form,
isDraft: boolean,
): Record<string, unknown> {
if (isDraft) return data;
const schema = form.schema as Record<string, unknown>;
const settings = schema?.settings as Record<string, unknown> | undefined;
const computedFields = settings?.computedFields as IComputedField[] | undefined;
if (!computedFields || computedFields.length === 0) return data;
const computedValues = calculateComputedFields(data, computedFields);
return { ...data, _computed: computedValues };
}
// Used in submitForm() and updateDraft()
const finalData = this.applyComputedFields(dto.data, form, isDraft);Response Mode
The form schema supports a responseMode setting that controls whether users can submit multiple responses.
Settings
| Mode | Description |
|------|-------------|
| multiple | Default. Users can submit unlimited responses |
| single | Each user can only submit once |
Tracking by Access Type
| Access Type | Tracking Method | Reliability |
|-------------|-----------------|-------------|
| authenticated | Server-side via submittedById | Reliable |
| action_group | Server-side via submittedById | Reliable |
| public | Client-side (frontend handles) | Best-effort only |
Backend Endpoint
For authenticated forms, the frontend calls hasUserSubmitted to check if the user has already submitted:
// FormResultController - POST /form-builder/result/has-submitted
@Post('has-submitted')
@UseGuards(JwtAuthGuard)
async hasUserSubmitted(
@Body() dto: GetMyDraftDto, // { formId: string }
@CurrentUser() user: ILoggedUserInfo,
): Promise<boolean> {
return this.formResultService.hasUserSubmitted(dto.formId, user);
}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.
Best Practices
Schema Design
- Store complete form schema including sections, fields, and settings
- Use schema versioning to track changes
- Store schema snapshots with results for historical accuracy
Access Control
- Use
publicsparingly - only for truly anonymous forms - Prefer
authenticatedfor most internal forms - Use
action_groupfor sensitive forms requiring specific permissions
Company Isolation
- Always set
companyIdwhen company feature is enabled - Use user's company context as default
- Allow explicit
companyIdin DTO for admin operations
Performance
- Use pagination when fetching results
- Select only needed fields in queries
- Consider caching frequently accessed forms
Swagger Configuration
The package includes a Swagger configuration helper that adapts documentation based on feature flags:
import { formBuilderSwaggerConfig } from '@flusys/nestjs-form-builder';
import { setupSwaggerDocs } from '@flusys/nestjs-core/docs';
// In bootstrap
const bootstrapConfig = { enableCompanyFeature: true };
setupSwaggerDocs(app, formBuilderSwaggerConfig(bootstrapConfig));Features:
- Automatically excludes
companyIdfields when company feature is disabled - Generates comprehensive API documentation
- Documents all access types and workflows
Schema Exclusions
When enableCompanyFeature: false, these schemas hide companyId:
CreateFormDtoUpdateFormDtoFormQueryDtoFormResponseDto
Permission Utilities
The package provides permission validation utilities for action group access:
import { validateUserPermissions } from '@flusys/nestjs-form-builder';
// Validate user has at least one of the required permissions
const hasPermission = await validateUserPermissions(
user, // ILoggedUserInfo
['hr.survey.submit', 'admin'], // Required permissions (OR logic)
cacheManager, // HybridCache instance
enableCompanyFeature, // boolean
logger, // Logger instance
'submitting form', // Context for audit logging
formId, // Resource ID for audit logging
);Features:
- Reads permissions from cache (same format as PermissionGuard)
- Fail-closed behavior: cache errors result in access denial
- Audit logging for permission denials
Permission Cache Key Format
// With company feature enabled
`permissions:company:${companyId}:branch:${branchId}:user:${userId}`
// Without company feature
`permissions:user:${userId}`Controller Security
Both controllers use createApiController with permission-based security:
Form Permissions
| Operation | Permission |
|-----------|------------|
| Create | FORM_PERMISSIONS.CREATE |
| Read | FORM_PERMISSIONS.READ |
| Update | FORM_PERMISSIONS.UPDATE |
| Delete | FORM_PERMISSIONS.DELETE |
Form Result Permissions
| Operation | Permission |
|-----------|------------|
| Create | FORM_RESULT_PERMISSIONS.CREATE |
| Read | FORM_RESULT_PERMISSIONS.READ |
| Update | FORM_RESULT_PERMISSIONS.UPDATE |
| Delete | FORM_RESULT_PERMISSIONS.DELETE |
Note: Submit endpoints (submit, submit-public) don't require these permissions - they use the form's accessType for authorization.
See Also
- ng-form-builder Guide - Frontend components
- Shared Guide - Base classes and utilities
- Auth Guide - User and company management
Last Updated: 2026-02-25
