@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.
- package/README.md +723 -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 +17 -63
- 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 +17 -63
- 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
|
@@ -39,29 +39,19 @@ function _ts_param(paramIndex, decorator) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
let FormBuilderConfigService = class FormBuilderConfigService {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*/ isCompanyFeatureEnabled() {
|
|
45
|
-
return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
42
|
+
isCompanyFeatureEnabled(tenant) {
|
|
43
|
+
return tenant?.enableCompanyFeature ?? this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
46
44
|
}
|
|
47
|
-
|
|
48
|
-
* Get database mode
|
|
49
|
-
*/ getDatabaseMode() {
|
|
45
|
+
getDatabaseMode() {
|
|
50
46
|
return this.options.bootstrapAppConfig?.databaseMode ?? 'single';
|
|
51
47
|
}
|
|
52
|
-
|
|
53
|
-
* Check if multi-tenant mode is enabled
|
|
54
|
-
*/ isMultiTenant() {
|
|
48
|
+
isMultiTenant() {
|
|
55
49
|
return this.getDatabaseMode() === 'multi-tenant';
|
|
56
50
|
}
|
|
57
|
-
|
|
58
|
-
* Get module options
|
|
59
|
-
*/ getOptions() {
|
|
51
|
+
getOptions() {
|
|
60
52
|
return this.options;
|
|
61
53
|
}
|
|
62
|
-
|
|
63
|
-
* Get form builder configuration
|
|
64
|
-
*/ getConfig() {
|
|
54
|
+
getConfig() {
|
|
65
55
|
return this.options.config;
|
|
66
56
|
}
|
|
67
57
|
constructor(options){
|
|
@@ -12,8 +12,7 @@ const _modules = require("@flusys/nestjs-shared/modules");
|
|
|
12
12
|
const _common = require("@nestjs/common");
|
|
13
13
|
const _core = require("@nestjs/core");
|
|
14
14
|
const _express = require("express");
|
|
15
|
-
const
|
|
16
|
-
const _formbuilderconstants = require("../config/form-builder.constants");
|
|
15
|
+
const _formbuilderconfigservice = require("./form-builder-config.service");
|
|
17
16
|
function _define_property(obj, key, value) {
|
|
18
17
|
if (key in obj) {
|
|
19
18
|
Object.defineProperty(obj, key, {
|
|
@@ -83,66 +82,28 @@ function _ts_param(paramIndex, decorator) {
|
|
|
83
82
|
};
|
|
84
83
|
}
|
|
85
84
|
let FormBuilderDataSourceProvider = class FormBuilderDataSourceProvider extends _modules.MultiTenantDataSourceService {
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Build parent options from FormBuilderModuleOptions
|
|
89
|
-
*/ static buildParentOptions(options) {
|
|
85
|
+
static buildParentOptions(options) {
|
|
90
86
|
return {
|
|
91
87
|
bootstrapAppConfig: options.bootstrapAppConfig,
|
|
92
|
-
defaultDatabaseConfig: options.config?.defaultDatabaseConfig
|
|
88
|
+
defaultDatabaseConfig: options.config?.defaultDatabaseConfig,
|
|
89
|
+
tenantDefaultDatabaseConfig: options.config?.tenantDefaultDatabaseConfig,
|
|
90
|
+
tenants: options.config?.tenants
|
|
93
91
|
};
|
|
94
92
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return this.formBuilderOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
93
|
+
async getFormBuilderEntities(enableCompanyFeature) {
|
|
94
|
+
const enable = enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
|
|
95
|
+
const { getFormBuilderEntitiesByConfig } = await Promise.resolve().then(()=>/*#__PURE__*/ _interop_require_wildcard(require("../entities")));
|
|
96
|
+
return getFormBuilderEntitiesByConfig(enable);
|
|
100
97
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* Falls back to global setting if not specified per-tenant
|
|
104
|
-
*/ getEnableCompanyFeatureForTenant(tenant) {
|
|
105
|
-
return tenant?.enableCompanyFeature ?? this.getEnableCompanyFeature();
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Get enable company feature for current request context
|
|
109
|
-
*/ getEnableCompanyFeatureForCurrentTenant() {
|
|
110
|
-
return this.getEnableCompanyFeatureForTenant(this.getCurrentTenant() ?? undefined);
|
|
111
|
-
}
|
|
112
|
-
// Entity Management
|
|
113
|
-
/**
|
|
114
|
-
* Get form builder entities based on company feature flag
|
|
115
|
-
*/ async getFormBuilderEntities(enableCompanyFeature) {
|
|
116
|
-
const enable = enableCompanyFeature ?? this.getEnableCompanyFeature();
|
|
117
|
-
const { Form, FormResult, FormWithCompany } = await Promise.resolve().then(()=>/*#__PURE__*/ _interop_require_wildcard(require("../entities")));
|
|
118
|
-
if (enable) {
|
|
119
|
-
return [
|
|
120
|
-
FormWithCompany,
|
|
121
|
-
FormResult
|
|
122
|
-
];
|
|
123
|
-
}
|
|
124
|
-
return [
|
|
125
|
-
Form,
|
|
126
|
-
FormResult
|
|
127
|
-
];
|
|
128
|
-
}
|
|
129
|
-
// Overrides
|
|
130
|
-
/**
|
|
131
|
-
* Override to dynamically set entities based on tenant config
|
|
132
|
-
*/ async createDataSourceFromConfig(config) {
|
|
133
|
-
const currentTenant = this.getCurrentTenant();
|
|
134
|
-
const enableCompanyFeature = this.getEnableCompanyFeatureForTenant(currentTenant ?? undefined);
|
|
98
|
+
async createDataSourceFromConfig(config) {
|
|
99
|
+
const enableCompanyFeature = this.configService.isCompanyFeatureEnabled(this.getCurrentTenant() ?? undefined);
|
|
135
100
|
const entities = await this.getFormBuilderEntities(enableCompanyFeature);
|
|
136
101
|
return super.createDataSourceFromConfig(config, entities);
|
|
137
102
|
}
|
|
138
|
-
|
|
139
|
-
* Override to use FormBuilder-specific static cache
|
|
140
|
-
*/ async getSingleDataSource() {
|
|
141
|
-
// Return existing initialized connection from FormBuilder-specific cache
|
|
103
|
+
async getSingleDataSource() {
|
|
142
104
|
if (FormBuilderDataSourceProvider.singleDataSource?.isInitialized) {
|
|
143
105
|
return FormBuilderDataSourceProvider.singleDataSource;
|
|
144
106
|
}
|
|
145
|
-
// If another request is creating the connection, wait for it
|
|
146
107
|
if (FormBuilderDataSourceProvider.singleConnectionLock) {
|
|
147
108
|
return FormBuilderDataSourceProvider.singleConnectionLock;
|
|
148
109
|
}
|
|
@@ -150,7 +111,6 @@ let FormBuilderDataSourceProvider = class FormBuilderDataSourceProvider extends
|
|
|
150
111
|
if (!config) {
|
|
151
112
|
throw new Error('No database config available. Provide defaultDatabaseConfig in FormBuilderModule options.');
|
|
152
113
|
}
|
|
153
|
-
// Create connection with lock to prevent race conditions
|
|
154
114
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
155
115
|
FormBuilderDataSourceProvider.singleConnectionLock = connectionPromise;
|
|
156
116
|
try {
|
|
@@ -161,20 +121,15 @@ let FormBuilderDataSourceProvider = class FormBuilderDataSourceProvider extends
|
|
|
161
121
|
FormBuilderDataSourceProvider.singleConnectionLock = null;
|
|
162
122
|
}
|
|
163
123
|
}
|
|
164
|
-
|
|
165
|
-
* Override to use FormBuilder-specific static cache for tenant connections
|
|
166
|
-
*/ async getOrCreateTenantConnection(tenant) {
|
|
167
|
-
// Return existing initialized connection from FormBuilder-specific cache
|
|
124
|
+
async getOrCreateTenantConnection(tenant) {
|
|
168
125
|
const existing = FormBuilderDataSourceProvider.tenantConnections.get(tenant.id);
|
|
169
126
|
if (existing?.isInitialized) {
|
|
170
127
|
return existing;
|
|
171
128
|
}
|
|
172
|
-
// If another request is creating this tenant's connection, wait for it
|
|
173
129
|
const pendingConnection = FormBuilderDataSourceProvider.connectionLocks.get(tenant.id);
|
|
174
130
|
if (pendingConnection) {
|
|
175
131
|
return pendingConnection;
|
|
176
132
|
}
|
|
177
|
-
// Create connection with lock to prevent race conditions
|
|
178
133
|
const config = this.buildTenantDatabaseConfig(tenant);
|
|
179
134
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
180
135
|
FormBuilderDataSourceProvider.connectionLocks.set(tenant.id, connectionPromise);
|
|
@@ -186,11 +141,10 @@ let FormBuilderDataSourceProvider = class FormBuilderDataSourceProvider extends
|
|
|
186
141
|
FormBuilderDataSourceProvider.connectionLocks.delete(tenant.id);
|
|
187
142
|
}
|
|
188
143
|
}
|
|
189
|
-
constructor(
|
|
190
|
-
super(FormBuilderDataSourceProvider.buildParentOptions(
|
|
144
|
+
constructor(configService, request){
|
|
145
|
+
super(FormBuilderDataSourceProvider.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0), _define_property(this, "logger", void 0), this.configService = configService, this.logger = new _common.Logger(FormBuilderDataSourceProvider.name);
|
|
191
146
|
}
|
|
192
147
|
};
|
|
193
|
-
// Override parent's static properties to have FormBuilder-specific cache
|
|
194
148
|
_define_property(FormBuilderDataSourceProvider, "tenantConnections", new Map());
|
|
195
149
|
_define_property(FormBuilderDataSourceProvider, "singleDataSource", null);
|
|
196
150
|
_define_property(FormBuilderDataSourceProvider, "tenantsRegistry", new Map());
|
|
@@ -201,12 +155,12 @@ FormBuilderDataSourceProvider = _ts_decorate([
|
|
|
201
155
|
(0, _common.Injectable)({
|
|
202
156
|
scope: _common.Scope.REQUEST
|
|
203
157
|
}),
|
|
204
|
-
_ts_param(0, (0, _common.Inject)(
|
|
158
|
+
_ts_param(0, (0, _common.Inject)(_formbuilderconfigservice.FormBuilderConfigService)),
|
|
205
159
|
_ts_param(1, (0, _common.Optional)()),
|
|
206
160
|
_ts_param(1, (0, _common.Inject)(_core.REQUEST)),
|
|
207
161
|
_ts_metadata("design:type", Function),
|
|
208
162
|
_ts_metadata("design:paramtypes", [
|
|
209
|
-
typeof
|
|
163
|
+
typeof _formbuilderconfigservice.FormBuilderConfigService === "undefined" ? Object : _formbuilderconfigservice.FormBuilderConfigService,
|
|
210
164
|
typeof _express.Request === "undefined" ? Object : _express.Request
|
|
211
165
|
])
|
|
212
166
|
], FormBuilderDataSourceProvider);
|
|
@@ -46,27 +46,68 @@ function _ts_param(paramIndex, decorator) {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
let FormResultService = class FormResultService extends _classes.RequestScopedApiService {
|
|
49
|
-
|
|
50
|
-
* Resolve entity class for this service
|
|
51
|
-
* @returns FormResult (same entity regardless of company feature)
|
|
52
|
-
*/ resolveEntity() {
|
|
49
|
+
resolveEntity() {
|
|
53
50
|
return _entities.FormResult;
|
|
54
51
|
}
|
|
55
|
-
|
|
56
|
-
* Get DataSource provider for this service
|
|
57
|
-
* @returns FormBuilderDataSourceProvider instance
|
|
58
|
-
*/ getDataSourceProvider() {
|
|
52
|
+
getDataSourceProvider() {
|
|
59
53
|
return this.dataSourceProvider;
|
|
60
54
|
}
|
|
61
|
-
|
|
62
|
-
* Initialize form repository for form lookups
|
|
63
|
-
*/ async ensureFormRepositoryInitialized() {
|
|
55
|
+
async ensureFormRepositoryInitialized() {
|
|
64
56
|
if (!this.formRepository) {
|
|
65
57
|
const enableCompanyFeature = this.formBuilderConfig.isCompanyFeatureEnabled();
|
|
66
58
|
const formEntity = enableCompanyFeature ? _entities.FormWithCompany : _entities.Form;
|
|
67
59
|
this.formRepository = await this.dataSourceProvider.getRepository(formEntity);
|
|
68
60
|
}
|
|
69
61
|
}
|
|
62
|
+
async getActiveForm(formId) {
|
|
63
|
+
await this.ensureFormRepositoryInitialized();
|
|
64
|
+
const form = await this.formRepository.findOne({
|
|
65
|
+
where: {
|
|
66
|
+
id: formId,
|
|
67
|
+
isActive: true,
|
|
68
|
+
deletedAt: (0, _typeorm.IsNull)()
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (!form) {
|
|
72
|
+
throw new _common.NotFoundException('Form not found or inactive');
|
|
73
|
+
}
|
|
74
|
+
return form;
|
|
75
|
+
}
|
|
76
|
+
applyCompanyFilterToQuery(query, user) {
|
|
77
|
+
if (this.formBuilderConfig.isCompanyFeatureEnabled() && user?.companyId) {
|
|
78
|
+
query.innerJoin('form', 'f', 'f.id = form_result.formId').andWhere('f.company_id = :companyId', {
|
|
79
|
+
companyId: user.companyId
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async validateSubmissionAccess(form, user, isPublic) {
|
|
84
|
+
if (isPublic) {
|
|
85
|
+
if (form.accessType !== _formaccesstypeenum.FormAccessType.PUBLIC) {
|
|
86
|
+
throw new _common.UnauthorizedException('This form is not available for public submission');
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (form.accessType === _formaccesstypeenum.FormAccessType.PUBLIC) return;
|
|
91
|
+
if (!user) {
|
|
92
|
+
throw new _common.UnauthorizedException('Authentication required to submit this form');
|
|
93
|
+
}
|
|
94
|
+
if (form.accessType === _formaccesstypeenum.FormAccessType.ACTION_GROUP && form.actionGroups?.length) {
|
|
95
|
+
const hasPermission = await (0, _permissionutils.validateUserPermissions)(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled(), this.logger, 'submitting form', form.id);
|
|
96
|
+
if (!hasPermission) {
|
|
97
|
+
throw new _common.ForbiddenException('You do not have permission to submit this form');
|
|
98
|
+
}
|
|
99
|
+
} else if (form.accessType !== _formaccesstypeenum.FormAccessType.AUTHENTICATED) {
|
|
100
|
+
throw new _common.BadRequestException('Invalid form access type');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
buildUserDraftWhere(formId, userId) {
|
|
104
|
+
return {
|
|
105
|
+
formId,
|
|
106
|
+
submittedById: userId,
|
|
107
|
+
isDraft: true,
|
|
108
|
+
deletedAt: (0, _typeorm.IsNull)()
|
|
109
|
+
};
|
|
110
|
+
}
|
|
70
111
|
// Query Customization
|
|
71
112
|
async getSelectQuery(query, _user, select) {
|
|
72
113
|
if (!select || !select.length) {
|
|
@@ -91,22 +132,28 @@ let FormResultService = class FormResultService extends _classes.RequestScopedAp
|
|
|
91
132
|
isRaw: false
|
|
92
133
|
};
|
|
93
134
|
}
|
|
94
|
-
|
|
95
|
-
* Override: Extra query manipulation - Auto-filter by user's company via Form
|
|
96
|
-
*/ async getExtraManipulateQuery(query, filterDto, user) {
|
|
135
|
+
async getExtraManipulateQuery(query, filterDto, user) {
|
|
97
136
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
98
|
-
|
|
99
|
-
const enableCompanyFeature = this.formBuilderConfig.isCompanyFeatureEnabled();
|
|
100
|
-
if (enableCompanyFeature && user?.companyId) {
|
|
101
|
-
query.innerJoin('form', 'f', 'f.id = form_result.formId').andWhere('f.company_id = :companyId', {
|
|
102
|
-
companyId: user.companyId
|
|
103
|
-
});
|
|
104
|
-
}
|
|
137
|
+
this.applyCompanyFilterToQuery(query, user);
|
|
105
138
|
return result;
|
|
106
139
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
applyComputedFields(data, form, isDraft) {
|
|
141
|
+
if (isDraft) {
|
|
142
|
+
return data;
|
|
143
|
+
}
|
|
144
|
+
const schema = form.schema;
|
|
145
|
+
const settings = schema?.settings;
|
|
146
|
+
const computedFields = settings?.computedFields;
|
|
147
|
+
if (!computedFields || computedFields.length === 0) {
|
|
148
|
+
return data;
|
|
149
|
+
}
|
|
150
|
+
const computedValues = (0, _computedfieldutils.calculateComputedFields)(data, computedFields);
|
|
151
|
+
return {
|
|
152
|
+
...data,
|
|
153
|
+
_computed: computedValues
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
convertEntityToResponseDto(entity, _isRaw) {
|
|
110
157
|
return {
|
|
111
158
|
id: entity.id,
|
|
112
159
|
formId: entity.formId,
|
|
@@ -126,137 +173,55 @@ let FormResultService = class FormResultService extends _classes.RequestScopedAp
|
|
|
126
173
|
};
|
|
127
174
|
}
|
|
128
175
|
// Form Submission
|
|
129
|
-
|
|
130
|
-
* Submit a form (authenticated or public)
|
|
131
|
-
*/ async submitForm(dto, user, isPublic = false) {
|
|
176
|
+
async submitForm(dto, user, isPublic = false) {
|
|
132
177
|
await this.ensureRepositoryInitialized();
|
|
133
|
-
await this.
|
|
134
|
-
|
|
135
|
-
const form = await this.formRepository.findOne({
|
|
136
|
-
where: {
|
|
137
|
-
id: dto.formId,
|
|
138
|
-
isActive: true,
|
|
139
|
-
deletedAt: (0, _typeorm.IsNull)()
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
if (!form) {
|
|
143
|
-
throw new _common.NotFoundException('Form not found or inactive');
|
|
144
|
-
}
|
|
145
|
-
// Validate access
|
|
146
|
-
if (isPublic) {
|
|
147
|
-
if (form.accessType !== _formaccesstypeenum.FormAccessType.PUBLIC) {
|
|
148
|
-
throw new _common.UnauthorizedException('This form is not available for public submission');
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
// For non-public submissions
|
|
152
|
-
switch(form.accessType){
|
|
153
|
-
case _formaccesstypeenum.FormAccessType.PUBLIC:
|
|
154
|
-
break;
|
|
155
|
-
case _formaccesstypeenum.FormAccessType.AUTHENTICATED:
|
|
156
|
-
if (!user) {
|
|
157
|
-
throw new _common.UnauthorizedException('Authentication required to submit this form');
|
|
158
|
-
}
|
|
159
|
-
break;
|
|
160
|
-
case _formaccesstypeenum.FormAccessType.ACTION_GROUP:
|
|
161
|
-
if (!user) {
|
|
162
|
-
throw new _common.UnauthorizedException('Authentication required to submit this form');
|
|
163
|
-
}
|
|
164
|
-
// Validate user has at least one of the required permissions
|
|
165
|
-
if (form.actionGroups?.length) {
|
|
166
|
-
const hasPermission = await (0, _permissionutils.validateUserPermissions)(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled(), this.logger, 'submitting form', form.id);
|
|
167
|
-
if (!hasPermission) {
|
|
168
|
-
throw new _common.ForbiddenException('You do not have permission to submit this form');
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
break;
|
|
172
|
-
default:
|
|
173
|
-
throw new _common.BadRequestException('Invalid form access type');
|
|
174
|
-
}
|
|
175
|
-
}
|
|
178
|
+
const form = await this.getActiveForm(dto.formId);
|
|
179
|
+
await this.validateSubmissionAccess(form, user, isPublic);
|
|
176
180
|
const isDraft = dto.isDraft ?? false;
|
|
177
|
-
//
|
|
181
|
+
// Handle existing draft for authenticated users
|
|
178
182
|
if (user?.id) {
|
|
179
|
-
// Check for existing draft
|
|
180
183
|
const existingDraft = await this.repository.findOne({
|
|
181
|
-
where:
|
|
182
|
-
formId: dto.formId,
|
|
183
|
-
submittedById: user.id,
|
|
184
|
-
isDraft: true,
|
|
185
|
-
deletedAt: (0, _typeorm.IsNull)()
|
|
186
|
-
}
|
|
184
|
+
where: this.buildUserDraftWhere(dto.formId, user.id)
|
|
187
185
|
});
|
|
188
186
|
if (existingDraft) {
|
|
189
187
|
if (isDraft) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
188
|
+
Object.assign(existingDraft, {
|
|
189
|
+
data: dto.data,
|
|
190
|
+
schemaVersionSnapshot: form.schema,
|
|
191
|
+
schemaVersion: form.schemaVersion,
|
|
192
|
+
submittedAt: new Date(),
|
|
193
|
+
metadata: dto.metadata ?? existingDraft.metadata
|
|
194
|
+
});
|
|
196
195
|
const saved = await this.repository.save(existingDraft);
|
|
197
196
|
return this.convertEntityToResponseDto(saved, false);
|
|
198
|
-
} else {
|
|
199
|
-
// Final submission: delete existing draft first
|
|
200
|
-
await this.repository.softRemove(existingDraft);
|
|
201
197
|
}
|
|
198
|
+
await this.repository.softRemove(existingDraft);
|
|
202
199
|
}
|
|
203
200
|
}
|
|
204
|
-
|
|
205
|
-
let finalData = dto.data;
|
|
206
|
-
if (!isDraft) {
|
|
207
|
-
const schema = form.schema;
|
|
208
|
-
const settings = schema?.settings;
|
|
209
|
-
const computedFields = settings?.computedFields;
|
|
210
|
-
if (computedFields && computedFields.length > 0) {
|
|
211
|
-
const computedValues = (0, _computedfieldutils.calculateComputedFields)(dto.data, computedFields);
|
|
212
|
-
// Merge computed values with submitted data (computed fields use their key as the property name)
|
|
213
|
-
finalData = {
|
|
214
|
-
...dto.data,
|
|
215
|
-
_computed: computedValues
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
// Create the result with schema snapshot
|
|
220
|
-
const resultData = {
|
|
201
|
+
const saved = await this.repository.save({
|
|
221
202
|
formId: dto.formId,
|
|
222
203
|
schemaVersionSnapshot: form.schema,
|
|
223
204
|
schemaVersion: form.schemaVersion,
|
|
224
|
-
data:
|
|
205
|
+
data: this.applyComputedFields(dto.data, form, isDraft),
|
|
225
206
|
submittedById: user?.id ?? null,
|
|
226
207
|
submittedAt: new Date(),
|
|
227
208
|
isDraft,
|
|
228
209
|
metadata: dto.metadata ?? null
|
|
229
|
-
};
|
|
230
|
-
const saved = await this.repository.save(resultData);
|
|
210
|
+
});
|
|
231
211
|
return this.convertEntityToResponseDto(saved, false);
|
|
232
212
|
}
|
|
233
|
-
|
|
234
|
-
* Get user's draft for a specific form
|
|
235
|
-
* Returns the most recent draft if exists, null otherwise
|
|
236
|
-
*/ async getMyDraft(formId, user) {
|
|
213
|
+
async getMyDraft(formId, user) {
|
|
237
214
|
await this.ensureRepositoryInitialized();
|
|
238
215
|
const draft = await this.repository.findOne({
|
|
239
|
-
where:
|
|
240
|
-
formId,
|
|
241
|
-
submittedById: user.id,
|
|
242
|
-
isDraft: true,
|
|
243
|
-
deletedAt: (0, _typeorm.IsNull)()
|
|
244
|
-
},
|
|
216
|
+
where: this.buildUserDraftWhere(formId, user.id),
|
|
245
217
|
order: {
|
|
246
218
|
updatedAt: 'DESC'
|
|
247
219
|
}
|
|
248
220
|
});
|
|
249
|
-
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
return this.convertEntityToResponseDto(draft, false);
|
|
221
|
+
return draft ? this.convertEntityToResponseDto(draft, false) : null;
|
|
253
222
|
}
|
|
254
|
-
|
|
255
|
-
* Update existing draft or convert to submission
|
|
256
|
-
*/ async updateDraft(draftId, dto, user) {
|
|
223
|
+
async updateDraft(draftId, dto, user) {
|
|
257
224
|
await this.ensureRepositoryInitialized();
|
|
258
|
-
await this.ensureFormRepositoryInitialized();
|
|
259
|
-
// Find the draft
|
|
260
225
|
const draft = await this.repository.findOne({
|
|
261
226
|
where: {
|
|
262
227
|
id: draftId,
|
|
@@ -267,74 +232,35 @@ let FormResultService = class FormResultService extends _classes.RequestScopedAp
|
|
|
267
232
|
if (!draft) {
|
|
268
233
|
throw new _common.NotFoundException('Draft not found');
|
|
269
234
|
}
|
|
270
|
-
|
|
271
|
-
const form = await this.formRepository.findOne({
|
|
272
|
-
where: {
|
|
273
|
-
id: dto.formId,
|
|
274
|
-
isActive: true,
|
|
275
|
-
deletedAt: (0, _typeorm.IsNull)()
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
if (!form) {
|
|
279
|
-
throw new _common.NotFoundException('Form not found or inactive');
|
|
280
|
-
}
|
|
235
|
+
const form = await this.getActiveForm(dto.formId);
|
|
281
236
|
const isDraft = dto.isDraft ?? false;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
finalData = {
|
|
291
|
-
...dto.data,
|
|
292
|
-
_computed: computedValues
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
// Update the draft
|
|
297
|
-
draft.data = finalData;
|
|
298
|
-
draft.schemaVersionSnapshot = form.schema;
|
|
299
|
-
draft.schemaVersion = form.schemaVersion;
|
|
300
|
-
draft.submittedAt = new Date();
|
|
301
|
-
draft.isDraft = isDraft;
|
|
302
|
-
draft.metadata = dto.metadata ?? draft.metadata;
|
|
237
|
+
Object.assign(draft, {
|
|
238
|
+
data: this.applyComputedFields(dto.data, form, isDraft),
|
|
239
|
+
schemaVersionSnapshot: form.schema,
|
|
240
|
+
schemaVersion: form.schemaVersion,
|
|
241
|
+
submittedAt: new Date(),
|
|
242
|
+
isDraft,
|
|
243
|
+
metadata: dto.metadata ?? draft.metadata
|
|
244
|
+
});
|
|
303
245
|
const saved = await this.repository.save(draft);
|
|
304
246
|
return this.convertEntityToResponseDto(saved, false);
|
|
305
247
|
}
|
|
306
|
-
|
|
307
|
-
* Get results for a specific form
|
|
308
|
-
*/ async getByFormId(formId, user, pagination) {
|
|
248
|
+
async getByFormId(formId, user, pagination) {
|
|
309
249
|
await this.ensureRepositoryInitialized();
|
|
310
|
-
const query = this.repository.createQueryBuilder(this.entityName)
|
|
311
|
-
query.where('form_result.formId = :formId', {
|
|
250
|
+
const query = this.repository.createQueryBuilder(this.entityName).where('form_result.formId = :formId', {
|
|
312
251
|
formId
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// Apply company filter via Form if enabled
|
|
316
|
-
const enableCompanyFeature = this.formBuilderConfig.isCompanyFeatureEnabled();
|
|
317
|
-
if (enableCompanyFeature && user?.companyId) {
|
|
318
|
-
query.innerJoin('form', 'f', 'f.id = form_result.formId').andWhere('f.company_id = :companyId', {
|
|
319
|
-
companyId: user.companyId
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
// Pagination
|
|
252
|
+
}).andWhere('form_result.deletedAt IS NULL');
|
|
253
|
+
this.applyCompanyFilterToQuery(query, user);
|
|
323
254
|
const page = pagination?.page ?? 0;
|
|
324
255
|
const pageSize = pagination?.pageSize ?? 10;
|
|
325
|
-
query.skip(page * pageSize).take(pageSize);
|
|
326
|
-
query.orderBy('form_result.submittedAt', 'DESC');
|
|
256
|
+
query.skip(page * pageSize).take(pageSize).orderBy('form_result.submittedAt', 'DESC');
|
|
327
257
|
const [entities, total] = await query.getManyAndCount();
|
|
328
|
-
const data = entities.map((e)=>this.convertEntityToResponseDto(e, false));
|
|
329
258
|
return {
|
|
330
|
-
data,
|
|
259
|
+
data: entities.map((e)=>this.convertEntityToResponseDto(e, false)),
|
|
331
260
|
total
|
|
332
261
|
};
|
|
333
262
|
}
|
|
334
|
-
|
|
335
|
-
* Check if user has already submitted this form (non-draft)
|
|
336
|
-
* Used for single response mode
|
|
337
|
-
*/ async hasUserSubmitted(formId, user) {
|
|
263
|
+
async hasUserSubmitted(formId, user) {
|
|
338
264
|
await this.ensureRepositoryInitialized();
|
|
339
265
|
const count = await this.repository.count({
|
|
340
266
|
where: {
|