@flusys/nestjs-form-builder 9.1.1 → 9.2.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 +21 -0
- package/config/message-keys.d.ts +1 -0
- package/controllers/form-result.controller.d.ts +1 -0
- package/controllers/form.controller.d.ts +1 -0
- package/fesm/432.js +245 -276
- package/fesm/config/index.js +2 -1
- package/fesm/entities/index.js +1 -1
- package/fesm/index.js +7 -3
- package/package.json +3 -3
- package/services/form-result.service.d.ts +6 -3
- package/services/form.service.d.ts +1 -0
- package/utils/computed-field.utils.d.ts +13 -37
package/README.md
CHANGED
|
@@ -204,6 +204,27 @@ FormBuilderModule.forRoot({
|
|
|
204
204
|
| `POST form-builder/result/submit-public` | `OptionalJwtGuard` | Now also accepts `EMAIL_VERIFIED` forms — pass `{ accessToken }` for anonymous visitors, nothing for JWT-authenticated ones |
|
|
205
205
|
| `POST form-builder/result/has-submitted-email` | `OptionalJwtGuard` | Anonymous-path counterpart to `has-submitted`, resolving identity the same way |
|
|
206
206
|
|
|
207
|
+
**Single-response race:** the "already submitted?" count and the insert are not atomic, so a non-draft submission to a `single` form `claim`s `form-builder:submit:[<tenant>:]<formId>:<email>` (30 s, released when done) on the shared cache. A concurrent submission for the same form + email - on any instance with `redis` / `hybrid` cache - gets 409 `form.result.already.submitted`. When the cache is unreachable the count check alone applies.
|
|
208
|
+
|
|
209
|
+
### Caching
|
|
210
|
+
|
|
211
|
+
`FormService` and `FormResultService` cache `get-all` / `get/:id` (ApiService version stamps, see [nestjs-shared section 7](../nestjs-shared/README.md#7-hybrid-cache)):
|
|
212
|
+
|
|
213
|
+
- Submit, draft save / finalize and `update-draft` bump the `form_result` stamp after the write - also when the insert fails after an old draft was already soft-removed - and before the domain event is published.
|
|
214
|
+
- With the company feature, cached result lists join `form` for the company filter, so `FormResultService` also depends on the `form` stamp.
|
|
215
|
+
- Public, authenticated, email-verified and submission reads (`public/:id`, `access-info/:id`, `authenticated/:id`, `email-verified/:id`, `by-slug`, submit) always read the database, so a deactivated or re-scoped form is never served from cache. OTP and access-token state lives only in `form_email_verification`, never in the cache.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 4. Computed Fields
|
|
220
|
+
|
|
221
|
+
`schema.settings.computedFields[]` are calculated on final submission (not drafts) and stored under `data._computed[key]`. Each field has ordered `rules` - `{ condition?, value }`, first match wins, else `defaultValue` - where `value` is an [Expression](../nestjs-shared/README.md#10-expressions) and `condition` an expression condition group. Combinations (regex extract, then join with another field, then upper-case; inline if / else) are just nested expressions.
|
|
222
|
+
|
|
223
|
+
- References: a field id, a field `name` (what templates use, `{{ email }}`), or `computed.<key>` for a computed field defined **earlier**.
|
|
224
|
+
- `valueType` (`number` | `string` | `boolean`) casts the result; a rule that fails at run time falls back to `defaultValue` instead of failing the submission.
|
|
225
|
+
- Saving a form validates its computed fields (`findComputedFieldIssue`): key format and uniqueness, shape, unknown references or functions, argument counts, literal regex patterns. A problem is a 400 with `form.invalid.computed.field` and `{ field, code, ... }`.
|
|
226
|
+
- **Breaking (v9):** the old rule shape (`computation: { type: 'direct' | 'field_reference' | 'arithmetic', config }` with `{ fieldId, comparison }` conditions) is not read or converted. A form saved with it computes only `defaultValue` on submit and is refused on save until its computed fields are rebuilt in the builder.
|
|
227
|
+
|
|
207
228
|
---
|
|
208
229
|
|
|
209
230
|
## License
|
package/config/message-keys.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const FORM_MESSAGES: {
|
|
|
15
15
|
readonly OTP_EXPIRED: "form.email.otp.expired";
|
|
16
16
|
readonly OTP_TOO_MANY_ATTEMPTS: "form.email.otp.too.many.attempts";
|
|
17
17
|
readonly OTP_VERIFIED: "form.email.otp.verified";
|
|
18
|
+
readonly INVALID_COMPUTED_FIELD: "form.invalid.computed.field";
|
|
18
19
|
};
|
|
19
20
|
export declare const FORM_RESULT_MESSAGES: {
|
|
20
21
|
readonly GET_ALL_SUCCESS: "form.result.get.all.success";
|
|
@@ -7,6 +7,7 @@ declare const FormResultController_base: abstract new (service: FormResultServic
|
|
|
7
7
|
readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
|
|
8
8
|
service: FormResultService;
|
|
9
9
|
isEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): boolean;
|
|
10
|
+
assertEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): void;
|
|
10
11
|
insert(addDto: CreateFormResultDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<FormResultResponseDto>>;
|
|
11
12
|
insertMany(addDto: CreateFormResultDto[], user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").BulkResponseDto<FormResultResponseDto>>;
|
|
12
13
|
getById(id: string, body: import("@flusys/nestjs-shared").GetByIdBodyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<FormResultResponseDto>>;
|
|
@@ -7,6 +7,7 @@ declare const FormController_base: abstract new (service: FormService) => {
|
|
|
7
7
|
readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
|
|
8
8
|
service: FormService;
|
|
9
9
|
isEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): boolean;
|
|
10
|
+
assertEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): void;
|
|
10
11
|
insert(addDto: CreateFormDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<FormResponseDto>>;
|
|
11
12
|
insertMany(addDto: CreateFormDto[], user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").BulkResponseDto<FormResponseDto>>;
|
|
12
13
|
getById(id: string, body: import("@flusys/nestjs-shared").GetByIdBodyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<FormResponseDto>>;
|
package/fesm/432.js
CHANGED
|
@@ -68,7 +68,8 @@ const FORM_MESSAGES = {
|
|
|
68
68
|
OTP_INVALID: 'form.email.otp.invalid',
|
|
69
69
|
OTP_EXPIRED: 'form.email.otp.expired',
|
|
70
70
|
OTP_TOO_MANY_ATTEMPTS: 'form.email.otp.too.many.attempts',
|
|
71
|
-
OTP_VERIFIED: 'form.email.otp.verified'
|
|
71
|
+
OTP_VERIFIED: 'form.email.otp.verified',
|
|
72
|
+
INVALID_COMPUTED_FIELD: 'form.invalid.computed.field'
|
|
72
73
|
};
|
|
73
74
|
const FORM_RESULT_MESSAGES = {
|
|
74
75
|
GET_ALL_SUCCESS: 'form.result.get.all.success',
|
|
@@ -428,7 +429,7 @@ _ts_decorate([
|
|
|
428
429
|
(0,external_typeorm_.Column)({
|
|
429
430
|
type: 'varchar',
|
|
430
431
|
length: 50,
|
|
431
|
-
default:
|
|
432
|
+
default: form_access_type_enum/* .FormAccessType.AUTHENTICATED */.h.AUTHENTICATED,
|
|
432
433
|
name: 'access_type'
|
|
433
434
|
}),
|
|
434
435
|
_ts_metadata("design:type", typeof form_access_type_enum/* .FormAccessType */.h === "undefined" ? Object : form_access_type_enum/* .FormAccessType */.h)
|
|
@@ -1173,16 +1174,16 @@ FormEmailAuthService = _ts_decorate([
|
|
|
1173
1174
|
/* import */ var _flusys_nestjs_shared_modules__rspack_import_3 = __webpack_require__(236);
|
|
1174
1175
|
/* import */ var _nestjs_common__rspack_import_4 = __webpack_require__(157);
|
|
1175
1176
|
/* import */ var typeorm__rspack_import_5 = __webpack_require__(158);
|
|
1176
|
-
/* import */ var
|
|
1177
|
-
/* import */ var
|
|
1177
|
+
/* import */ var _config_index_js__rspack_import_13 = __webpack_require__(769);
|
|
1178
|
+
/* import */ var _config_index_js__rspack_import_14 = __webpack_require__(137);
|
|
1178
1179
|
/* import */ var _dtos_form_result_dto_js__rspack_import_6 = __webpack_require__(788);
|
|
1179
1180
|
/* import */ var _entities_index_js__rspack_import_7 = __webpack_require__(814);
|
|
1180
|
-
/* import */ var
|
|
1181
|
-
/* import */ var
|
|
1182
|
-
/* import */ var
|
|
1183
|
-
/* import */ var
|
|
1184
|
-
/* import */ var
|
|
1185
|
-
/* import */ var
|
|
1181
|
+
/* import */ var _enums_form_access_type_enum_js__rspack_import_15 = __webpack_require__(376);
|
|
1182
|
+
/* import */ var _utils_computed_field_utils_js__rspack_import_8 = __webpack_require__(801);
|
|
1183
|
+
/* import */ var _utils_permission_utils_js__rspack_import_9 = __webpack_require__(988);
|
|
1184
|
+
/* import */ var _form_builder_config_service_js__rspack_import_10 = __webpack_require__(781);
|
|
1185
|
+
/* import */ var _form_builder_datasource_provider_js__rspack_import_11 = __webpack_require__(593);
|
|
1186
|
+
/* import */ var _form_email_auth_service_js__rspack_import_12 = __webpack_require__(474);
|
|
1186
1187
|
function _ts_decorate(decorators, target, key, desc) {
|
|
1187
1188
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1188
1189
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
@@ -1230,14 +1231,20 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1230
1231
|
permissionResolver;
|
|
1231
1232
|
permissionCache;
|
|
1232
1233
|
formRepository = null;
|
|
1233
|
-
|
|
1234
|
-
//
|
|
1235
|
-
|
|
1236
|
-
// per-instance lock would never be shared between the two racing requests.
|
|
1237
|
-
// Does not span multiple app instances; see withSubmissionLock().
|
|
1238
|
-
static submissionLocks = new Map();
|
|
1234
|
+
resultLogger = new _nestjs_common__rspack_import_4.Logger(FormResultService.name);
|
|
1235
|
+
// Bounds how long a crashed instance can block a submitter's retry.
|
|
1236
|
+
static SUBMISSION_CLAIM_TTL_MS = 30_000;
|
|
1239
1237
|
constructor(cacheManager, utilsService, formBuilderConfig, dataSourceProvider, formEmailAuthService, permissionResolver, permissionCache){
|
|
1240
|
-
super(
|
|
1238
|
+
super(_config_index_js__rspack_import_13/* .FORM_BUILDER_EVENT_ENTITIES.FORM_RESULT */.eF.FORM_RESULT, cacheManager, utilsService, FormResultService.name, true, _config_index_js__rspack_import_13/* .FORM_BUILDER_EVENT_MODULE */.T$, dataSourceProvider, _entities_index_js__rspack_import_7/* .FormResult */.ez), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.dataSourceProvider = dataSourceProvider, this.formEmailAuthService = formEmailAuthService, this.permissionResolver = permissionResolver, this.permissionCache = permissionCache;
|
|
1239
|
+
}
|
|
1240
|
+
// With the company feature, cached lists join the form to filter by its company.
|
|
1241
|
+
cacheDependencies() {
|
|
1242
|
+
return this.formBuilderConfig.isCompanyFeatureEnabled() ? [
|
|
1243
|
+
this.entityName,
|
|
1244
|
+
_config_index_js__rspack_import_13/* .FORM_BUILDER_EVENT_ENTITIES.FORM */.eF.FORM
|
|
1245
|
+
] : [
|
|
1246
|
+
this.entityName
|
|
1247
|
+
];
|
|
1241
1248
|
}
|
|
1242
1249
|
async ensureFormRepositoryInitialized() {
|
|
1243
1250
|
if (!this.formRepository) {
|
|
@@ -1246,21 +1253,31 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1246
1253
|
this.formRepository = await this.dataSourceProvider.getRepository(formEntity);
|
|
1247
1254
|
}
|
|
1248
1255
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1256
|
+
// Single-response forms: the count check and the insert are not atomic, so a
|
|
1257
|
+
// double-click or two tabs (possibly on different instances) could both pass the
|
|
1258
|
+
// check. The cross-instance claim lets only one submission per form + email run;
|
|
1259
|
+
// a concurrent one is rejected as already submitted. Without a reachable cache the
|
|
1260
|
+
// count check alone still applies.
|
|
1261
|
+
async withSubmissionClaim(formId, email, fn) {
|
|
1262
|
+
const tenant = this.cacheTenant();
|
|
1263
|
+
const claimKey = `form-builder:submit:${tenant ? `${tenant}:` : ''}${formId}:${email}`;
|
|
1264
|
+
let claimed = false;
|
|
1265
|
+
try {
|
|
1266
|
+
claimed = await this.cacheManager.claim(claimKey, FormResultService.SUBMISSION_CLAIM_TTL_MS);
|
|
1267
|
+
if (!claimed) {
|
|
1268
|
+
throw new _nestjs_common__rspack_import_4.ConflictException({
|
|
1269
|
+
message: 'You have already submitted a response to this form',
|
|
1270
|
+
messageKey: _config_index_js__rspack_import_14/* .FORM_RESULT_MESSAGES.ALREADY_SUBMITTED */.y.ALREADY_SUBMITTED
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
if (error instanceof _nestjs_common__rspack_import_4.ConflictException) throw error;
|
|
1275
|
+
this.resultLogger.warn(`Submission claim unavailable for form '${formId}': ${error.message}`);
|
|
1276
|
+
}
|
|
1257
1277
|
try {
|
|
1258
1278
|
return await fn();
|
|
1259
1279
|
} finally{
|
|
1260
|
-
|
|
1261
|
-
if (FormResultService.submissionLocks.get(key) === current) {
|
|
1262
|
-
FormResultService.submissionLocks.delete(key);
|
|
1263
|
-
}
|
|
1280
|
+
if (claimed) await this.cacheManager.releaseClaim(claimKey).catch(()=>undefined);
|
|
1264
1281
|
}
|
|
1265
1282
|
}
|
|
1266
1283
|
async getActiveForm(formId) {
|
|
@@ -1275,88 +1292,93 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1275
1292
|
if (!form) {
|
|
1276
1293
|
throw new _nestjs_common__rspack_import_4.NotFoundException({
|
|
1277
1294
|
message: 'Form not found or inactive',
|
|
1278
|
-
messageKey:
|
|
1295
|
+
messageKey: _config_index_js__rspack_import_14/* .FORM_MESSAGES.NOT_FOUND */.Q.NOT_FOUND
|
|
1279
1296
|
});
|
|
1280
1297
|
}
|
|
1281
1298
|
return form;
|
|
1282
1299
|
}
|
|
1283
1300
|
applyCompanyFilterToQuery(query, user) {
|
|
1284
|
-
if (this.formBuilderConfig.isCompanyFeatureEnabled()
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1301
|
+
if (!this.formBuilderConfig.isCompanyFeatureEnabled() || !user) return;
|
|
1302
|
+
if (!user.companyId) {
|
|
1303
|
+
query.andWhere('1 = 0');
|
|
1304
|
+
return;
|
|
1288
1305
|
}
|
|
1306
|
+
query.innerJoin('form', 'f', 'f.id = form_result.formId').andWhere('f.company_id = :companyId', {
|
|
1307
|
+
companyId: user.companyId
|
|
1308
|
+
});
|
|
1289
1309
|
}
|
|
1290
1310
|
static PUBLICLY_SUBMITTABLE_ACCESS_TYPES = new Set([
|
|
1291
|
-
|
|
1292
|
-
|
|
1311
|
+
_enums_form_access_type_enum_js__rspack_import_15/* .FormAccessType.PUBLIC */.h.PUBLIC,
|
|
1312
|
+
_enums_form_access_type_enum_js__rspack_import_15/* .FormAccessType.EMAIL_VERIFIED */.h.EMAIL_VERIFIED
|
|
1293
1313
|
]);
|
|
1294
1314
|
async validateSubmissionAccess(form, user, isPublic) {
|
|
1295
1315
|
if (isPublic) {
|
|
1296
1316
|
if (!FormResultService.PUBLICLY_SUBMITTABLE_ACCESS_TYPES.has(form.accessType)) {
|
|
1297
1317
|
throw new _nestjs_common__rspack_import_4.UnauthorizedException({
|
|
1298
1318
|
message: 'This form is not available for public submission',
|
|
1299
|
-
messageKey:
|
|
1319
|
+
messageKey: _config_index_js__rspack_import_14/* .FORM_MESSAGES.NOT_PUBLIC */.Q.NOT_PUBLIC
|
|
1300
1320
|
});
|
|
1301
1321
|
}
|
|
1302
1322
|
return;
|
|
1303
1323
|
}
|
|
1304
1324
|
switch(form.accessType){
|
|
1305
|
-
case
|
|
1325
|
+
case _enums_form_access_type_enum_js__rspack_import_15/* .FormAccessType.PUBLIC */.h.PUBLIC:
|
|
1306
1326
|
return;
|
|
1307
|
-
case
|
|
1327
|
+
case _enums_form_access_type_enum_js__rspack_import_15/* .FormAccessType.EMAIL_VERIFIED */.h.EMAIL_VERIFIED:
|
|
1308
1328
|
throw new _nestjs_common__rspack_import_4.BadRequestException({
|
|
1309
1329
|
message: 'Email-verified forms must be submitted through the public endpoint',
|
|
1310
|
-
messageKey:
|
|
1330
|
+
messageKey: _config_index_js__rspack_import_14/* .FORM_MESSAGES.INVALID_ACCESS_TYPE */.Q.INVALID_ACCESS_TYPE
|
|
1311
1331
|
});
|
|
1312
|
-
case
|
|
1313
|
-
case
|
|
1332
|
+
case _enums_form_access_type_enum_js__rspack_import_15/* .FormAccessType.ACTION_GROUP */.h.ACTION_GROUP:
|
|
1333
|
+
case _enums_form_access_type_enum_js__rspack_import_15/* .FormAccessType.AUTHENTICATED */.h.AUTHENTICATED:
|
|
1314
1334
|
break;
|
|
1315
1335
|
default:
|
|
1316
1336
|
throw new _nestjs_common__rspack_import_4.BadRequestException({
|
|
1317
1337
|
message: 'Invalid form access type',
|
|
1318
|
-
messageKey:
|
|
1338
|
+
messageKey: _config_index_js__rspack_import_14/* .FORM_MESSAGES.INVALID_ACCESS_TYPE */.Q.INVALID_ACCESS_TYPE
|
|
1319
1339
|
});
|
|
1320
1340
|
}
|
|
1321
1341
|
if (!user) {
|
|
1322
1342
|
throw new _nestjs_common__rspack_import_4.UnauthorizedException({
|
|
1323
1343
|
message: 'Authentication required to submit this form',
|
|
1324
|
-
messageKey:
|
|
1344
|
+
messageKey: _config_index_js__rspack_import_14/* .FORM_MESSAGES.AUTH_REQUIRED */.Q.AUTH_REQUIRED
|
|
1325
1345
|
});
|
|
1326
1346
|
}
|
|
1327
|
-
if (form.accessType ===
|
|
1328
|
-
const permitted = await (0,
|
|
1347
|
+
if (form.accessType === _enums_form_access_type_enum_js__rspack_import_15/* .FormAccessType.ACTION_GROUP */.h.ACTION_GROUP && form.permissionLogic) {
|
|
1348
|
+
const permitted = await (0,_utils_permission_utils_js__rspack_import_9/* .hasFormPermission */.E)(user, form.permissionLogic, {
|
|
1329
1349
|
resolver: this.permissionResolver,
|
|
1330
1350
|
cache: this.permissionCache
|
|
1331
1351
|
});
|
|
1332
1352
|
if (!permitted) {
|
|
1333
1353
|
throw new _nestjs_common__rspack_import_4.ForbiddenException({
|
|
1334
1354
|
message: 'You do not have permission to submit this form',
|
|
1335
|
-
messageKey:
|
|
1355
|
+
messageKey: _config_index_js__rspack_import_14/* .FORM_MESSAGES.ACCESS_DENIED */.Q.ACCESS_DENIED
|
|
1336
1356
|
});
|
|
1337
1357
|
}
|
|
1338
|
-
} else if (form.accessType !==
|
|
1358
|
+
} else if (form.accessType !== _enums_form_access_type_enum_js__rspack_import_15/* .FormAccessType.AUTHENTICATED */.h.AUTHENTICATED) {
|
|
1339
1359
|
throw new _nestjs_common__rspack_import_4.BadRequestException({
|
|
1340
1360
|
message: 'Invalid form access type',
|
|
1341
|
-
messageKey:
|
|
1361
|
+
messageKey: _config_index_js__rspack_import_14/* .FORM_MESSAGES.INVALID_ACCESS_TYPE */.Q.INVALID_ACCESS_TYPE
|
|
1342
1362
|
});
|
|
1343
1363
|
}
|
|
1344
1364
|
}
|
|
1345
1365
|
async resolveEmailVerifiedSubmitter(form, user, accessToken) {
|
|
1346
|
-
if (form.accessType !==
|
|
1366
|
+
if (form.accessType !== _enums_form_access_type_enum_js__rspack_import_15/* .FormAccessType.EMAIL_VERIFIED */.h.EMAIL_VERIFIED) return null;
|
|
1347
1367
|
const email = await this.formEmailAuthService.resolveSubmitterEmail(form.id, user ?? undefined, accessToken);
|
|
1348
1368
|
if (!email) {
|
|
1349
1369
|
throw new _nestjs_common__rspack_import_4.UnauthorizedException({
|
|
1350
1370
|
message: 'Email verification required to submit this form',
|
|
1351
|
-
messageKey:
|
|
1371
|
+
messageKey: _config_index_js__rspack_import_14/* .FORM_MESSAGES.EMAIL_VERIFICATION_REQUIRED */.Q.EMAIL_VERIFICATION_REQUIRED
|
|
1352
1372
|
});
|
|
1353
1373
|
}
|
|
1354
1374
|
return email;
|
|
1355
1375
|
}
|
|
1356
|
-
|
|
1357
|
-
if (!submitterEmail) return;
|
|
1376
|
+
isSingleResponse(form) {
|
|
1358
1377
|
const settings = form.schema?.settings;
|
|
1359
|
-
|
|
1378
|
+
return settings?.responseMode === 'single';
|
|
1379
|
+
}
|
|
1380
|
+
async assertSingleResponseNotExceeded(form, submitterEmail) {
|
|
1381
|
+
if (!submitterEmail || !this.isSingleResponse(form)) return;
|
|
1360
1382
|
const count = await this.repository.count({
|
|
1361
1383
|
where: {
|
|
1362
1384
|
formId: form.id,
|
|
@@ -1368,7 +1390,7 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1368
1390
|
if (count > 0) {
|
|
1369
1391
|
throw new _nestjs_common__rspack_import_4.ConflictException({
|
|
1370
1392
|
message: 'You have already submitted a response to this form',
|
|
1371
|
-
messageKey:
|
|
1393
|
+
messageKey: _config_index_js__rspack_import_14/* .FORM_RESULT_MESSAGES.ALREADY_SUBMITTED */.y.ALREADY_SUBMITTED
|
|
1372
1394
|
});
|
|
1373
1395
|
}
|
|
1374
1396
|
}
|
|
@@ -1413,13 +1435,10 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1413
1435
|
if (isDraft) {
|
|
1414
1436
|
return data;
|
|
1415
1437
|
}
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1418
|
-
const computedFields = settings?.computedFields;
|
|
1419
|
-
if (!computedFields || computedFields.length === 0) {
|
|
1438
|
+
const computedValues = (0,_utils_computed_field_utils_js__rspack_import_8/* .calculateComputedFields */.XZ)(data, form.schema);
|
|
1439
|
+
if (Object.keys(computedValues).length === 0) {
|
|
1420
1440
|
return data;
|
|
1421
1441
|
}
|
|
1422
|
-
const computedValues = (0,_utils_computed_field_utils_js__rspack_import_15/* .calculateComputedFields */.X)(data, computedFields);
|
|
1423
1442
|
return {
|
|
1424
1443
|
...data,
|
|
1425
1444
|
_computed: computedValues
|
|
@@ -1452,11 +1471,8 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1452
1471
|
const submitterEmail = await this.resolveEmailVerifiedSubmitter(form, user, dto.accessToken);
|
|
1453
1472
|
const isDraft = dto.isDraft ?? false;
|
|
1454
1473
|
const persist = ()=>this.persistSubmission(form, dto, user, submitterEmail, isDraft);
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
// same form + email (see withSubmissionLock()).
|
|
1458
|
-
if (!isDraft && submitterEmail) {
|
|
1459
|
-
return this.withSubmissionLock(`${form.id}:${submitterEmail}`, persist);
|
|
1474
|
+
if (!isDraft && submitterEmail && this.isSingleResponse(form)) {
|
|
1475
|
+
return this.withSubmissionClaim(form.id, submitterEmail, persist);
|
|
1460
1476
|
}
|
|
1461
1477
|
return persist();
|
|
1462
1478
|
}
|
|
@@ -1464,8 +1480,17 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1464
1480
|
if (!isDraft) {
|
|
1465
1481
|
await this.assertSingleResponseNotExceeded(form, submitterEmail);
|
|
1466
1482
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1483
|
+
let saved;
|
|
1484
|
+
try {
|
|
1485
|
+
saved = await this.writeSubmission(form, dto, user, submitterEmail, isDraft);
|
|
1486
|
+
} finally{
|
|
1487
|
+
// Also after a failed insert: the replaced draft may already be soft-removed.
|
|
1488
|
+
await this.invalidateCache();
|
|
1489
|
+
}
|
|
1490
|
+
await this.publishSubmissionEvent(saved, dto.formId, saved.isDraft, user);
|
|
1491
|
+
return this.convertEntityToResponseDto(saved, false);
|
|
1492
|
+
}
|
|
1493
|
+
async writeSubmission(form, dto, user, submitterEmail, isDraft) {
|
|
1469
1494
|
if (user?.id) {
|
|
1470
1495
|
const existingDraft = await this.repository.findOne({
|
|
1471
1496
|
where: this.buildUserDraftWhere(dto.formId, user.id)
|
|
@@ -1478,18 +1503,12 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1478
1503
|
schemaVersion: form.schemaVersion,
|
|
1479
1504
|
submittedAt: new Date()
|
|
1480
1505
|
});
|
|
1481
|
-
|
|
1482
|
-
await this.invalidateResultCache([
|
|
1483
|
-
saved
|
|
1484
|
-
]);
|
|
1485
|
-
await this.publishSubmissionEvent(saved, dto.formId, true, user);
|
|
1486
|
-
return this.convertEntityToResponseDto(saved, false);
|
|
1506
|
+
return await this.repository.save(existingDraft);
|
|
1487
1507
|
}
|
|
1488
1508
|
await this.repository.softRemove(existingDraft);
|
|
1489
|
-
staleResults.push(existingDraft);
|
|
1490
1509
|
}
|
|
1491
1510
|
}
|
|
1492
|
-
|
|
1511
|
+
return await this.repository.save({
|
|
1493
1512
|
formId: dto.formId,
|
|
1494
1513
|
schemaVersionSnapshot: form.schema,
|
|
1495
1514
|
schemaVersion: form.schemaVersion,
|
|
@@ -1499,12 +1518,6 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1499
1518
|
submittedAt: new Date(),
|
|
1500
1519
|
isDraft
|
|
1501
1520
|
});
|
|
1502
|
-
await this.invalidateResultCache([
|
|
1503
|
-
...staleResults,
|
|
1504
|
-
saved
|
|
1505
|
-
]);
|
|
1506
|
-
await this.publishSubmissionEvent(saved, dto.formId, isDraft, user);
|
|
1507
|
-
return this.convertEntityToResponseDto(saved, false);
|
|
1508
1521
|
}
|
|
1509
1522
|
async getMyDraft(formId, user) {
|
|
1510
1523
|
await this.ensureDataSourceRepository();
|
|
@@ -1521,14 +1534,16 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1521
1534
|
const draft = await this.repository.findOne({
|
|
1522
1535
|
where: {
|
|
1523
1536
|
id: draftId,
|
|
1537
|
+
formId: dto.formId,
|
|
1524
1538
|
submittedById: user.id,
|
|
1539
|
+
isDraft: true,
|
|
1525
1540
|
deletedAt: (0,typeorm__rspack_import_5.IsNull)()
|
|
1526
1541
|
}
|
|
1527
1542
|
});
|
|
1528
1543
|
if (!draft) {
|
|
1529
1544
|
throw new _nestjs_common__rspack_import_4.NotFoundException({
|
|
1530
1545
|
message: 'Draft not found',
|
|
1531
|
-
messageKey:
|
|
1546
|
+
messageKey: _config_index_js__rspack_import_14/* .FORM_RESULT_MESSAGES.NOT_FOUND */.y.NOT_FOUND
|
|
1532
1547
|
});
|
|
1533
1548
|
}
|
|
1534
1549
|
const form = await this.getActiveForm(dto.formId);
|
|
@@ -1541,14 +1556,12 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1541
1556
|
isDraft
|
|
1542
1557
|
});
|
|
1543
1558
|
const saved = await this.repository.save(draft);
|
|
1544
|
-
await this.
|
|
1545
|
-
saved
|
|
1546
|
-
]);
|
|
1559
|
+
await this.invalidateCache();
|
|
1547
1560
|
await this.publishSubmissionEvent(saved, dto.formId, isDraft, user);
|
|
1548
1561
|
return this.convertEntityToResponseDto(saved, false);
|
|
1549
1562
|
}
|
|
1550
1563
|
async publishSubmissionEvent(result, formId, isDraft, user) {
|
|
1551
|
-
await this.publishDomainAction(isDraft ?
|
|
1564
|
+
await this.publishDomainAction(isDraft ? _config_index_js__rspack_import_13/* .FORM_BUILDER_EVENT_ACTIONS.DRAFT_SAVED */.ov.DRAFT_SAVED : _config_index_js__rspack_import_13/* .FORM_BUILDER_EVENT_ACTIONS.SUBMITTED */.ov.SUBMITTED, {
|
|
1552
1565
|
ids: [
|
|
1553
1566
|
result.id
|
|
1554
1567
|
],
|
|
@@ -1561,12 +1574,6 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
|
|
|
1561
1574
|
user
|
|
1562
1575
|
});
|
|
1563
1576
|
}
|
|
1564
|
-
async invalidateResultCache(results) {
|
|
1565
|
-
await Promise.all([
|
|
1566
|
-
this.clearCacheForAll(),
|
|
1567
|
-
this.clearCacheForId(results)
|
|
1568
|
-
]);
|
|
1569
|
-
}
|
|
1570
1577
|
async getByFormId(formId, user, pagination) {
|
|
1571
1578
|
await this.ensureDataSourceRepository();
|
|
1572
1579
|
const query = this.repository.createQueryBuilder(this.entityName).where('form_result.formId = :formId', {
|
|
@@ -1641,9 +1648,9 @@ FormResultService = _ts_decorate([
|
|
|
1641
1648
|
}),
|
|
1642
1649
|
_ts_param(0, (0,_nestjs_common__rspack_import_4.Inject)('CACHE_INSTANCE')),
|
|
1643
1650
|
_ts_param(1, (0,_nestjs_common__rspack_import_4.Inject)(_flusys_nestjs_shared_modules__rspack_import_3.UtilsService)),
|
|
1644
|
-
_ts_param(2, (0,_nestjs_common__rspack_import_4.Inject)(
|
|
1645
|
-
_ts_param(3, (0,_nestjs_common__rspack_import_4.Inject)(
|
|
1646
|
-
_ts_param(4, (0,_nestjs_common__rspack_import_4.Inject)(
|
|
1651
|
+
_ts_param(2, (0,_nestjs_common__rspack_import_4.Inject)(_form_builder_config_service_js__rspack_import_10/* .FormBuilderConfigService */.j)),
|
|
1652
|
+
_ts_param(3, (0,_nestjs_common__rspack_import_4.Inject)(_form_builder_datasource_provider_js__rspack_import_11/* .FormBuilderDataSourceProvider */._)),
|
|
1653
|
+
_ts_param(4, (0,_nestjs_common__rspack_import_4.Inject)(_form_email_auth_service_js__rspack_import_12/* .FormEmailAuthService */.w)),
|
|
1647
1654
|
_ts_param(5, (0,_nestjs_common__rspack_import_4.Optional)()),
|
|
1648
1655
|
_ts_param(5, (0,_nestjs_common__rspack_import_4.Inject)(_flusys_nestjs_shared_interfaces__rspack_import_2.PERMISSION_RESOLVER)),
|
|
1649
1656
|
_ts_param(6, (0,_nestjs_common__rspack_import_4.Optional)()),
|
|
@@ -1652,9 +1659,9 @@ FormResultService = _ts_decorate([
|
|
|
1652
1659
|
_ts_metadata("design:paramtypes", [
|
|
1653
1660
|
typeof _flusys_nestjs_shared_classes__rspack_import_0.HybridCache === "undefined" ? Object : _flusys_nestjs_shared_classes__rspack_import_0.HybridCache,
|
|
1654
1661
|
typeof _flusys_nestjs_shared_modules__rspack_import_3.UtilsService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_3.UtilsService,
|
|
1655
|
-
typeof
|
|
1656
|
-
typeof
|
|
1657
|
-
typeof
|
|
1662
|
+
typeof _form_builder_config_service_js__rspack_import_10/* .FormBuilderConfigService */.j === "undefined" ? Object : _form_builder_config_service_js__rspack_import_10/* .FormBuilderConfigService */.j,
|
|
1663
|
+
typeof _form_builder_datasource_provider_js__rspack_import_11/* .FormBuilderDataSourceProvider */._ === "undefined" ? Object : _form_builder_datasource_provider_js__rspack_import_11/* .FormBuilderDataSourceProvider */._,
|
|
1664
|
+
typeof _form_email_auth_service_js__rspack_import_12/* .FormEmailAuthService */.w === "undefined" ? Object : _form_email_auth_service_js__rspack_import_12/* .FormEmailAuthService */.w,
|
|
1658
1665
|
typeof IPermissionResolver === "undefined" ? Object : IPermissionResolver,
|
|
1659
1666
|
typeof _flusys_nestjs_shared_modules__rspack_import_3.SharedPermissionCacheService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_3.SharedPermissionCacheService
|
|
1660
1667
|
])
|
|
@@ -1695,6 +1702,8 @@ var message_keys = __webpack_require__(137);
|
|
|
1695
1702
|
var entities = __webpack_require__(814);
|
|
1696
1703
|
// EXTERNAL MODULE: ./projects/nestjs-form-builder/src/enums/form-access-type.enum.ts
|
|
1697
1704
|
var form_access_type_enum = __webpack_require__(376);
|
|
1705
|
+
// EXTERNAL MODULE: ./projects/nestjs-form-builder/src/utils/computed-field.utils.ts
|
|
1706
|
+
var computed_field_utils = __webpack_require__(801);
|
|
1698
1707
|
// EXTERNAL MODULE: ./projects/nestjs-form-builder/src/utils/permission.utils.ts
|
|
1699
1708
|
var permission_utils = __webpack_require__(988);
|
|
1700
1709
|
// EXTERNAL MODULE: ./projects/nestjs-form-builder/src/services/form-builder-config.service.ts
|
|
@@ -1738,6 +1747,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
1738
1747
|
|
|
1739
1748
|
|
|
1740
1749
|
|
|
1750
|
+
|
|
1741
1751
|
class FormService extends classes_.ApiService {
|
|
1742
1752
|
cacheManager;
|
|
1743
1753
|
utilsService;
|
|
@@ -1751,6 +1761,9 @@ class FormService extends classes_.ApiService {
|
|
|
1751
1761
|
async convertSingleDtoToEntity(dto, user) {
|
|
1752
1762
|
let form = {};
|
|
1753
1763
|
let isUpdate = false;
|
|
1764
|
+
if (dto.schema) {
|
|
1765
|
+
this.assertValidComputedFields(dto.schema);
|
|
1766
|
+
}
|
|
1754
1767
|
// NOTE: Using 'id' in dto check instead of instanceof - instanceof may not work after esbuild bundling
|
|
1755
1768
|
if ('id' in dto && dto.id && typeof dto.id === 'string') {
|
|
1756
1769
|
const dbData = await this.repository.findOne({
|
|
@@ -1776,6 +1789,9 @@ class FormService extends classes_.ApiService {
|
|
|
1776
1789
|
...form,
|
|
1777
1790
|
...dto
|
|
1778
1791
|
};
|
|
1792
|
+
if (!isUpdate) {
|
|
1793
|
+
form.accessType ??= form_access_type_enum/* .FormAccessType.AUTHENTICATED */.h.AUTHENTICATED;
|
|
1794
|
+
}
|
|
1779
1795
|
// Set company context if enabled
|
|
1780
1796
|
const enableCompanyFeature = this.formBuilderConfig.isCompanyFeatureEnabled();
|
|
1781
1797
|
if (enableCompanyFeature) {
|
|
@@ -1795,6 +1811,19 @@ class FormService extends classes_.ApiService {
|
|
|
1795
1811
|
}
|
|
1796
1812
|
return form;
|
|
1797
1813
|
}
|
|
1814
|
+
assertValidComputedFields(schema) {
|
|
1815
|
+
const issue = (0,computed_field_utils/* .findComputedFieldIssue */.Qc)(schema);
|
|
1816
|
+
if (!issue) return;
|
|
1817
|
+
throw new common_.BadRequestException({
|
|
1818
|
+
message: `Computed field "${issue.field}" is invalid (${issue.code}).`,
|
|
1819
|
+
messageKey: message_keys/* .FORM_MESSAGES.INVALID_COMPUTED_FIELD */.Q.INVALID_COMPUTED_FIELD,
|
|
1820
|
+
messageVariables: {
|
|
1821
|
+
field: issue.field,
|
|
1822
|
+
code: issue.code,
|
|
1823
|
+
...issue.variables
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1798
1827
|
async getSelectQuery(query, _user, select) {
|
|
1799
1828
|
if (!select || !select.length) {
|
|
1800
1829
|
select = [
|
|
@@ -2074,204 +2103,144 @@ FormService = _ts_decorate([
|
|
|
2074
2103
|
|
|
2075
2104
|
},
|
|
2076
2105
|
801(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
* NOTE: Interface definitions here mirror @flusys/ng-form-builder interfaces
|
|
2084
|
-
* but are defined separately to avoid cross-package dependencies.
|
|
2085
|
-
* Both use compatible JSON structures for form schema serialization.
|
|
2086
|
-
*
|
|
2087
|
-
* @see FLUSYS_NG/projects/ng-form-builder/interfaces/computed-field.interface.ts
|
|
2088
|
-
*/ // ============================================================================
|
|
2089
|
-
// Type Definitions
|
|
2090
|
-
// ============================================================================
|
|
2091
|
-
// ============================================================================
|
|
2092
|
-
// Type Guards & Helpers
|
|
2093
|
-
// ============================================================================
|
|
2094
|
-
function isObject(value) {
|
|
2095
|
-
return typeof value === 'object' && value !== null;
|
|
2096
|
-
}
|
|
2097
|
-
function isDirectValueConfig(config) {
|
|
2098
|
-
return isObject(config) && config.type === 'direct' && 'value' in config;
|
|
2106
|
+
/* import */ var _flusys_nestjs_shared_utils__rspack_import_0 = __webpack_require__(216);
|
|
2107
|
+
|
|
2108
|
+
/** A field reference to an earlier computed field: `computed.<key>`. */ const COMPUTED_REF_PREFIX = 'computed.';
|
|
2109
|
+
const COMPUTED_KEY_PATTERN = /^[A-Za-z_]\w*$/;
|
|
2110
|
+
function asSchema(schema) {
|
|
2111
|
+
return typeof schema === 'object' && schema !== null ? schema : {};
|
|
2099
2112
|
}
|
|
2100
|
-
function
|
|
2101
|
-
return
|
|
2113
|
+
function formFields(schema) {
|
|
2114
|
+
return (schema.sections ?? []).flatMap((section)=>section.fields ?? []).filter((field)=>typeof field?.id === 'string');
|
|
2102
2115
|
}
|
|
2103
|
-
function
|
|
2104
|
-
|
|
2116
|
+
/** References resolve by field id, by field name (what templates use), or `computed.<key>`. */ function createResolver(data, fields, computed) {
|
|
2117
|
+
const idByName = new Map(fields.filter((f)=>f.name).map((f)=>[
|
|
2118
|
+
f.name,
|
|
2119
|
+
f.id
|
|
2120
|
+
]));
|
|
2121
|
+
return (ref)=>{
|
|
2122
|
+
if (ref.startsWith(COMPUTED_REF_PREFIX)) {
|
|
2123
|
+
const key = ref.slice(COMPUTED_REF_PREFIX.length);
|
|
2124
|
+
return Object.hasOwn(computed, key) ? computed[key] : undefined;
|
|
2125
|
+
}
|
|
2126
|
+
if (Object.hasOwn(data, ref)) return data[ref];
|
|
2127
|
+
const id = idByName.get(ref);
|
|
2128
|
+
return id !== undefined && Object.hasOwn(data, id) ? data[id] : undefined;
|
|
2129
|
+
};
|
|
2105
2130
|
}
|
|
2106
|
-
function
|
|
2107
|
-
|
|
2131
|
+
function castValue(value, valueType) {
|
|
2132
|
+
if (value === null || value === undefined) return null;
|
|
2133
|
+
switch(valueType){
|
|
2134
|
+
case 'number':
|
|
2135
|
+
try {
|
|
2136
|
+
return (0,_flusys_nestjs_shared_utils__rspack_import_0.expressionToNumber)(value);
|
|
2137
|
+
} catch {
|
|
2138
|
+
return null;
|
|
2139
|
+
}
|
|
2140
|
+
case 'boolean':
|
|
2141
|
+
if (typeof value === 'boolean') return value;
|
|
2142
|
+
return [
|
|
2143
|
+
'true',
|
|
2144
|
+
'1',
|
|
2145
|
+
'yes'
|
|
2146
|
+
].includes(((0,_flusys_nestjs_shared_utils__rspack_import_0.expressionToText)(value) ?? '').trim().toLowerCase());
|
|
2147
|
+
default:
|
|
2148
|
+
return (0,_flusys_nestjs_shared_utils__rspack_import_0.expressionToText)(value);
|
|
2149
|
+
}
|
|
2108
2150
|
}
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
return
|
|
2151
|
+
function computeField(field, ctx) {
|
|
2152
|
+
for (const rule of field.rules ?? []){
|
|
2153
|
+
if (!rule.condition || (0,_flusys_nestjs_shared_utils__rspack_import_0.evaluateConditionGroup)(rule.condition, ctx)) return (0,_flusys_nestjs_shared_utils__rspack_import_0.evaluateExpression)(rule.value, ctx);
|
|
2154
|
+
}
|
|
2155
|
+
return field.defaultValue ?? null;
|
|
2114
2156
|
}
|
|
2115
2157
|
/**
|
|
2116
|
-
*
|
|
2117
|
-
*
|
|
2118
|
-
*
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
if (!computedFields || computedFields.length === 0) {
|
|
2123
|
-
return {};
|
|
2124
|
-
}
|
|
2158
|
+
* Computed values for a submission, keyed by each computed field's `key`. Fields run in order,
|
|
2159
|
+
* so a field may use earlier ones (`computed.<key>`); a field whose rules fail at run time gets
|
|
2160
|
+
* its default value instead of failing the submission.
|
|
2161
|
+
*/ function calculateComputedFields(data, schema) {
|
|
2162
|
+
const parsed = asSchema(schema);
|
|
2163
|
+
const computedFields = parsed.settings?.computedFields ?? [];
|
|
2125
2164
|
const result = {};
|
|
2165
|
+
if (computedFields.length === 0) return result;
|
|
2166
|
+
const ctx = {
|
|
2167
|
+
resolveRef: createResolver(data, formFields(parsed), result)
|
|
2168
|
+
};
|
|
2126
2169
|
for (const field of computedFields){
|
|
2170
|
+
let value;
|
|
2127
2171
|
try {
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
// If calculation fails, use default value or null
|
|
2132
|
-
result[field.key] = field.defaultValue ?? null;
|
|
2172
|
+
value = computeField(field, ctx);
|
|
2173
|
+
} catch {
|
|
2174
|
+
value = field.defaultValue ?? null;
|
|
2133
2175
|
}
|
|
2176
|
+
result[field.key] = castValue(value, field.valueType);
|
|
2134
2177
|
}
|
|
2135
2178
|
return result;
|
|
2136
2179
|
}
|
|
2137
|
-
/**
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2180
|
+
/** First problem in the schema's computed fields, or null. Run on save so authors see it there, not at submit. */ function findComputedFieldIssue(schema) {
|
|
2181
|
+
const parsed = asSchema(schema);
|
|
2182
|
+
const computedFields = parsed.settings?.computedFields ?? [];
|
|
2183
|
+
const fields = formFields(parsed);
|
|
2184
|
+
const knownRefs = new Set(fields.flatMap((f)=>f.name ? [
|
|
2185
|
+
f.id,
|
|
2186
|
+
f.name
|
|
2187
|
+
] : [
|
|
2188
|
+
f.id
|
|
2189
|
+
]));
|
|
2190
|
+
const earlierKeys = new Set();
|
|
2191
|
+
for (const field of computedFields){
|
|
2192
|
+
const label = String(field?.name || field?.key || '');
|
|
2193
|
+
if (typeof field?.key !== 'string' || !COMPUTED_KEY_PATTERN.test(field.key)) {
|
|
2194
|
+
return {
|
|
2195
|
+
field: label,
|
|
2196
|
+
code: 'invalid_key',
|
|
2197
|
+
variables: {
|
|
2198
|
+
key: String(field?.key ?? '')
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2144
2201
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
return isEmptyValue(fieldValue);
|
|
2177
|
-
case 'is_not_empty':
|
|
2178
|
-
return !isEmptyValue(fieldValue);
|
|
2179
|
-
case 'contains':
|
|
2180
|
-
return String(fieldValue).includes(String(compareValue));
|
|
2181
|
-
case 'not_contains':
|
|
2182
|
-
return !String(fieldValue).includes(String(compareValue));
|
|
2183
|
-
case 'starts_with':
|
|
2184
|
-
return String(fieldValue).startsWith(String(compareValue));
|
|
2185
|
-
case 'ends_with':
|
|
2186
|
-
return String(fieldValue).endsWith(String(compareValue));
|
|
2187
|
-
case 'greater_than':
|
|
2188
|
-
return Number(fieldValue) > Number(compareValue);
|
|
2189
|
-
case 'less_than':
|
|
2190
|
-
return Number(fieldValue) < Number(compareValue);
|
|
2191
|
-
case 'greater_or_equal':
|
|
2192
|
-
return Number(fieldValue) >= Number(compareValue);
|
|
2193
|
-
case 'less_or_equal':
|
|
2194
|
-
return Number(fieldValue) <= Number(compareValue);
|
|
2195
|
-
case 'is_before':
|
|
2196
|
-
return new Date(String(fieldValue)) < new Date(String(compareValue));
|
|
2197
|
-
case 'is_after':
|
|
2198
|
-
return new Date(String(fieldValue)) > new Date(String(compareValue));
|
|
2199
|
-
case 'is_checked':
|
|
2200
|
-
return fieldValue === true || fieldValue === 'true' || fieldValue === 1;
|
|
2201
|
-
case 'is_not_checked':
|
|
2202
|
-
return fieldValue === false || fieldValue === 'false' || fieldValue === 0 || !fieldValue;
|
|
2203
|
-
case 'is_any_of':
|
|
2204
|
-
case 'in':
|
|
2205
|
-
return Array.isArray(compareValue) && compareValue.includes(fieldValue);
|
|
2206
|
-
case 'is_none_of':
|
|
2207
|
-
case 'not_in':
|
|
2208
|
-
return !Array.isArray(compareValue) || !compareValue.includes(fieldValue);
|
|
2209
|
-
default:
|
|
2210
|
-
return false;
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
/**
|
|
2214
|
-
* Execute a computation and return the result
|
|
2215
|
-
*/ function executeComputation(formData, computation, valueType) {
|
|
2216
|
-
const { config } = computation;
|
|
2217
|
-
// Use type guards for safe access
|
|
2218
|
-
if (computation.type === 'direct' && isDirectValueConfig(config)) {
|
|
2219
|
-
return valueType === 'number' ? Number(config.value) : String(config.value);
|
|
2220
|
-
}
|
|
2221
|
-
if (computation.type === 'field_reference' && isFieldReferenceConfig(config)) {
|
|
2222
|
-
const value = formData[config.fieldId];
|
|
2223
|
-
if (value === null || value === undefined) {
|
|
2224
|
-
return null;
|
|
2202
|
+
if (earlierKeys.has(field.key)) return {
|
|
2203
|
+
field: label,
|
|
2204
|
+
code: 'duplicate_key',
|
|
2205
|
+
variables: {
|
|
2206
|
+
key: field.key
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
if (!Array.isArray(field.rules)) return {
|
|
2210
|
+
field: label,
|
|
2211
|
+
code: 'invalid_node',
|
|
2212
|
+
variables: {
|
|
2213
|
+
type: 'rules'
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
2216
|
+
const isKnownRef = (ref)=>knownRefs.has(ref) || ref.startsWith(COMPUTED_REF_PREFIX) && earlierKeys.has(ref.slice(COMPUTED_REF_PREFIX.length));
|
|
2217
|
+
try {
|
|
2218
|
+
for (const rule of field.rules){
|
|
2219
|
+
if (rule.condition) (0,_flusys_nestjs_shared_utils__rspack_import_0.assertValidConditionGroup)(rule.condition, {
|
|
2220
|
+
isKnownRef
|
|
2221
|
+
});
|
|
2222
|
+
(0,_flusys_nestjs_shared_utils__rspack_import_0.assertValidExpression)(rule.value, {
|
|
2223
|
+
isKnownRef
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
} catch (error) {
|
|
2227
|
+
if ((0,_flusys_nestjs_shared_utils__rspack_import_0.isExpressionError)(error)) return {
|
|
2228
|
+
field: label,
|
|
2229
|
+
code: error.code,
|
|
2230
|
+
variables: error.variables
|
|
2231
|
+
};
|
|
2232
|
+
throw error;
|
|
2225
2233
|
}
|
|
2226
|
-
|
|
2234
|
+
earlierKeys.add(field.key);
|
|
2227
2235
|
}
|
|
2228
|
-
if (computation.type === 'arithmetic' && isArithmeticConfig(config)) {
|
|
2229
|
-
return calculateArithmetic(formData, config);
|
|
2230
|
-
}
|
|
2231
|
-
// Unknown computation type
|
|
2232
2236
|
return null;
|
|
2233
2237
|
}
|
|
2234
|
-
/**
|
|
2235
|
-
* Perform arithmetic calculation
|
|
2236
|
-
*/ function calculateArithmetic(formData, config) {
|
|
2237
|
-
const values = config.operands.map((operand)=>{
|
|
2238
|
-
if (operand.type === 'constant') {
|
|
2239
|
-
return operand.value ?? 0;
|
|
2240
|
-
} else {
|
|
2241
|
-
const fieldValue = formData[operand.fieldId ?? ''];
|
|
2242
|
-
return fieldValue !== null && fieldValue !== undefined ? Number(fieldValue) : 0;
|
|
2243
|
-
}
|
|
2244
|
-
});
|
|
2245
|
-
if (values.length === 0) {
|
|
2246
|
-
return null;
|
|
2247
|
-
}
|
|
2248
|
-
switch(config.operation){
|
|
2249
|
-
case 'sum':
|
|
2250
|
-
case 'increment':
|
|
2251
|
-
return values.reduce((acc, val)=>acc + val, 0);
|
|
2252
|
-
case 'subtract':
|
|
2253
|
-
case 'decrement':
|
|
2254
|
-
return values.reduce((acc, val, idx)=>idx === 0 ? val : acc - val, 0);
|
|
2255
|
-
case 'multiply':
|
|
2256
|
-
return values.reduce((acc, val)=>acc * val, 1);
|
|
2257
|
-
case 'divide':
|
|
2258
|
-
{
|
|
2259
|
-
const [first, ...rest] = values;
|
|
2260
|
-
return rest.reduce((acc, val)=>val !== 0 ? acc / val : acc, first);
|
|
2261
|
-
}
|
|
2262
|
-
case 'average':
|
|
2263
|
-
return values.reduce((acc, val)=>acc + val, 0) / values.length;
|
|
2264
|
-
case 'min':
|
|
2265
|
-
return Math.min(...values);
|
|
2266
|
-
case 'max':
|
|
2267
|
-
return Math.max(...values);
|
|
2268
|
-
default:
|
|
2269
|
-
return null;
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
2238
|
|
|
2273
2239
|
__webpack_require__.d(__webpack_exports__, {
|
|
2274
|
-
|
|
2240
|
+
Qc: () => (findComputedFieldIssue),
|
|
2241
|
+
XZ: () => (calculateComputedFields)
|
|
2242
|
+
}, {
|
|
2243
|
+
xI: COMPUTED_REF_PREFIX
|
|
2275
2244
|
});
|
|
2276
2245
|
|
|
2277
2246
|
|
package/fesm/config/index.js
CHANGED
|
@@ -71,7 +71,8 @@ const FORM_MESSAGES = {
|
|
|
71
71
|
OTP_INVALID: 'form.email.otp.invalid',
|
|
72
72
|
OTP_EXPIRED: 'form.email.otp.expired',
|
|
73
73
|
OTP_TOO_MANY_ATTEMPTS: 'form.email.otp.too.many.attempts',
|
|
74
|
-
OTP_VERIFIED: 'form.email.otp.verified'
|
|
74
|
+
OTP_VERIFIED: 'form.email.otp.verified',
|
|
75
|
+
INVALID_COMPUTED_FIELD: 'form.invalid.computed.field'
|
|
75
76
|
};
|
|
76
77
|
const FORM_RESULT_MESSAGES = {
|
|
77
78
|
GET_ALL_SUCCESS: 'form.result.get.all.success',
|
package/fesm/entities/index.js
CHANGED
|
@@ -104,7 +104,7 @@ _ts_decorate([
|
|
|
104
104
|
(0,external_typeorm_.Column)({
|
|
105
105
|
type: 'varchar',
|
|
106
106
|
length: 50,
|
|
107
|
-
default:
|
|
107
|
+
default: form_access_type_enum/* .FormAccessType.AUTHENTICATED */.h.AUTHENTICATED,
|
|
108
108
|
name: 'access_type'
|
|
109
109
|
}),
|
|
110
110
|
_ts_metadata("design:type", typeof form_access_type_enum/* .FormAccessType */.h === "undefined" ? Object : form_access_type_enum/* .FormAccessType */.h)
|
package/fesm/index.js
CHANGED
|
@@ -194,6 +194,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
194
194
|
|
|
195
195
|
// EXPORTS
|
|
196
196
|
__webpack_require__.d(__webpack_exports__, {
|
|
197
|
+
xI: () => (/* reexport */ computed_field_utils/* .COMPUTED_REF_PREFIX */.xI),
|
|
197
198
|
YD: () => (/* reexport */ dtos/* .CreateFormDto */.YD),
|
|
198
199
|
pZ: () => (/* reexport */ dtos/* .CreateFormResultDto */.pZ),
|
|
199
200
|
d7: () => (/* reexport */ dtos/* .EmailVerifiedFormResponseDto */.d7),
|
|
@@ -236,7 +237,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
236
237
|
Uj: () => (/* reexport */ dtos/* .UpdateFormResultDto */.Uj),
|
|
237
238
|
uY: () => (/* reexport */ dtos/* .VerifyFormEmailOtpDto */.uY),
|
|
238
239
|
TU: () => (/* reexport */ dtos/* .VerifyOtpResponseDto */.TU),
|
|
239
|
-
XZ: () => (/* reexport */ computed_field_utils/* .calculateComputedFields */.
|
|
240
|
+
XZ: () => (/* reexport */ computed_field_utils/* .calculateComputedFields */.XZ),
|
|
241
|
+
Qc: () => (/* reexport */ computed_field_utils/* .findComputedFieldIssue */.Qc),
|
|
240
242
|
aY: () => (/* reexport */ docs/* .formBuilderSwaggerConfig */.a),
|
|
241
243
|
iO: () => (/* reexport */ entities.getFormBuilderEntitiesByConfig),
|
|
242
244
|
EY: () => (/* reexport */ permission_utils/* .hasFormPermission */.E)
|
|
@@ -488,7 +490,8 @@ import * as __rspack_chunk_2 from './720.js';
|
|
|
488
490
|
__webpack_require__.C(__rspack_chunk_2);
|
|
489
491
|
import * as __rspack_chunk_3 from './index.js';
|
|
490
492
|
__webpack_require__.C(__rspack_chunk_3);
|
|
491
|
-
var __webpack_exports__ = __webpack_exec__(851);var
|
|
493
|
+
var __webpack_exports__ = __webpack_exec__(851);var __webpack_exports__COMPUTED_REF_PREFIX = __webpack_exports__.xI;
|
|
494
|
+
var __webpack_exports__CreateFormDto = __webpack_exports__.YD;
|
|
492
495
|
var __webpack_exports__CreateFormResultDto = __webpack_exports__.pZ;
|
|
493
496
|
var __webpack_exports__EmailVerifiedFormResponseDto = __webpack_exports__.d7;
|
|
494
497
|
var __webpack_exports__FORM_BUILDER_EMAIL_PROVIDER = __webpack_exports__.Q3;
|
|
@@ -531,7 +534,8 @@ var __webpack_exports__UpdateFormResultDto = __webpack_exports__.Uj;
|
|
|
531
534
|
var __webpack_exports__VerifyFormEmailOtpDto = __webpack_exports__.uY;
|
|
532
535
|
var __webpack_exports__VerifyOtpResponseDto = __webpack_exports__.TU;
|
|
533
536
|
var __webpack_exports__calculateComputedFields = __webpack_exports__.XZ;
|
|
537
|
+
var __webpack_exports__findComputedFieldIssue = __webpack_exports__.Qc;
|
|
534
538
|
var __webpack_exports__formBuilderSwaggerConfig = __webpack_exports__.aY;
|
|
535
539
|
var __webpack_exports__getFormBuilderEntitiesByConfig = __webpack_exports__.iO;
|
|
536
540
|
var __webpack_exports__hasFormPermission = __webpack_exports__.EY;
|
|
537
|
-
export { __webpack_exports__CreateFormDto as CreateFormDto, __webpack_exports__CreateFormResultDto as CreateFormResultDto, __webpack_exports__EmailVerifiedFormResponseDto as EmailVerifiedFormResponseDto, __webpack_exports__FORM_BUILDER_EMAIL_PROVIDER as FORM_BUILDER_EMAIL_PROVIDER, __webpack_exports__FORM_BUILDER_EVENT_ACTIONS as FORM_BUILDER_EVENT_ACTIONS, __webpack_exports__FORM_BUILDER_EVENT_ENTITIES as FORM_BUILDER_EVENT_ENTITIES, __webpack_exports__FORM_BUILDER_EVENT_MODULE as FORM_BUILDER_EVENT_MODULE, __webpack_exports__FORM_BUILDER_MODULE_OPTIONS as FORM_BUILDER_MODULE_OPTIONS, __webpack_exports__FORM_MESSAGES as FORM_MESSAGES, __webpack_exports__FORM_RESULT_MESSAGES as FORM_RESULT_MESSAGES, __webpack_exports__Form as Form, __webpack_exports__FormAccessInfoResponseDto as FormAccessInfoResponseDto, __webpack_exports__FormAccessType as FormAccessType, __webpack_exports__FormBase as FormBase, __webpack_exports__FormBuilderConfigService as FormBuilderConfigService, __webpack_exports__FormBuilderDataSourceProvider as FormBuilderDataSourceProvider, __webpack_exports__FormBuilderModule as FormBuilderModule, __webpack_exports__FormCompanyEntities as FormCompanyEntities, __webpack_exports__FormController as FormController, __webpack_exports__FormCoreEntities as FormCoreEntities, __webpack_exports__FormEmailAuthController as FormEmailAuthController, __webpack_exports__FormEmailAuthService as FormEmailAuthService, __webpack_exports__FormEmailVerification as FormEmailVerification, __webpack_exports__FormResponseDto as FormResponseDto, __webpack_exports__FormResult as FormResult, __webpack_exports__FormResultController as FormResultController, __webpack_exports__FormResultResponseDto as FormResultResponseDto, __webpack_exports__FormResultService as FormResultService, __webpack_exports__FormService as FormService, __webpack_exports__FormWithCompany as FormWithCompany, __webpack_exports__GetMyDraftDto as GetMyDraftDto, __webpack_exports__GetResultsByFormDto as GetResultsByFormDto, __webpack_exports__HasSubmittedEmailDto as HasSubmittedEmailDto, __webpack_exports__LoadEmailVerifiedFormDto as LoadEmailVerifiedFormDto, __webpack_exports__PublicFormResponseDto as PublicFormResponseDto, __webpack_exports__RequestFormEmailOtpDto as RequestFormEmailOtpDto, __webpack_exports__SubmitFormDto as SubmitFormDto, __webpack_exports__UpdateDraftDto as UpdateDraftDto, __webpack_exports__UpdateFormDto as UpdateFormDto, __webpack_exports__UpdateFormResultDto as UpdateFormResultDto, __webpack_exports__VerifyFormEmailOtpDto as VerifyFormEmailOtpDto, __webpack_exports__VerifyOtpResponseDto as VerifyOtpResponseDto, __webpack_exports__calculateComputedFields as calculateComputedFields, __webpack_exports__formBuilderSwaggerConfig as formBuilderSwaggerConfig, __webpack_exports__getFormBuilderEntitiesByConfig as getFormBuilderEntitiesByConfig, __webpack_exports__hasFormPermission as hasFormPermission };
|
|
541
|
+
export { __webpack_exports__COMPUTED_REF_PREFIX as COMPUTED_REF_PREFIX, __webpack_exports__CreateFormDto as CreateFormDto, __webpack_exports__CreateFormResultDto as CreateFormResultDto, __webpack_exports__EmailVerifiedFormResponseDto as EmailVerifiedFormResponseDto, __webpack_exports__FORM_BUILDER_EMAIL_PROVIDER as FORM_BUILDER_EMAIL_PROVIDER, __webpack_exports__FORM_BUILDER_EVENT_ACTIONS as FORM_BUILDER_EVENT_ACTIONS, __webpack_exports__FORM_BUILDER_EVENT_ENTITIES as FORM_BUILDER_EVENT_ENTITIES, __webpack_exports__FORM_BUILDER_EVENT_MODULE as FORM_BUILDER_EVENT_MODULE, __webpack_exports__FORM_BUILDER_MODULE_OPTIONS as FORM_BUILDER_MODULE_OPTIONS, __webpack_exports__FORM_MESSAGES as FORM_MESSAGES, __webpack_exports__FORM_RESULT_MESSAGES as FORM_RESULT_MESSAGES, __webpack_exports__Form as Form, __webpack_exports__FormAccessInfoResponseDto as FormAccessInfoResponseDto, __webpack_exports__FormAccessType as FormAccessType, __webpack_exports__FormBase as FormBase, __webpack_exports__FormBuilderConfigService as FormBuilderConfigService, __webpack_exports__FormBuilderDataSourceProvider as FormBuilderDataSourceProvider, __webpack_exports__FormBuilderModule as FormBuilderModule, __webpack_exports__FormCompanyEntities as FormCompanyEntities, __webpack_exports__FormController as FormController, __webpack_exports__FormCoreEntities as FormCoreEntities, __webpack_exports__FormEmailAuthController as FormEmailAuthController, __webpack_exports__FormEmailAuthService as FormEmailAuthService, __webpack_exports__FormEmailVerification as FormEmailVerification, __webpack_exports__FormResponseDto as FormResponseDto, __webpack_exports__FormResult as FormResult, __webpack_exports__FormResultController as FormResultController, __webpack_exports__FormResultResponseDto as FormResultResponseDto, __webpack_exports__FormResultService as FormResultService, __webpack_exports__FormService as FormService, __webpack_exports__FormWithCompany as FormWithCompany, __webpack_exports__GetMyDraftDto as GetMyDraftDto, __webpack_exports__GetResultsByFormDto as GetResultsByFormDto, __webpack_exports__HasSubmittedEmailDto as HasSubmittedEmailDto, __webpack_exports__LoadEmailVerifiedFormDto as LoadEmailVerifiedFormDto, __webpack_exports__PublicFormResponseDto as PublicFormResponseDto, __webpack_exports__RequestFormEmailOtpDto as RequestFormEmailOtpDto, __webpack_exports__SubmitFormDto as SubmitFormDto, __webpack_exports__UpdateDraftDto as UpdateDraftDto, __webpack_exports__UpdateFormDto as UpdateFormDto, __webpack_exports__UpdateFormResultDto as UpdateFormResultDto, __webpack_exports__VerifyFormEmailOtpDto as VerifyFormEmailOtpDto, __webpack_exports__VerifyOtpResponseDto as VerifyOtpResponseDto, __webpack_exports__calculateComputedFields as calculateComputedFields, __webpack_exports__findComputedFieldIssue as findComputedFieldIssue, __webpack_exports__formBuilderSwaggerConfig as formBuilderSwaggerConfig, __webpack_exports__getFormBuilderEntitiesByConfig as getFormBuilderEntitiesByConfig, __webpack_exports__hasFormPermission as hasFormPermission };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-form-builder",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.0",
|
|
4
4
|
"description": "Dynamic form builder module with schema versioning and access control",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./fesm/index.js",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"express": "^5.0.0"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@flusys/nestjs-core": "9.
|
|
79
|
-
"@flusys/nestjs-shared": "9.
|
|
78
|
+
"@flusys/nestjs-core": "9.2.0",
|
|
79
|
+
"@flusys/nestjs-shared": "9.2.0"
|
|
80
80
|
}
|
|
81
81
|
}
|
|
@@ -18,15 +18,18 @@ export declare class FormResultService extends ApiService<CreateFormResultDto, U
|
|
|
18
18
|
private readonly permissionResolver?;
|
|
19
19
|
private readonly permissionCache?;
|
|
20
20
|
private formRepository;
|
|
21
|
-
private
|
|
21
|
+
private readonly resultLogger;
|
|
22
|
+
private static readonly SUBMISSION_CLAIM_TTL_MS;
|
|
22
23
|
constructor(cacheManager: HybridCache, utilsService: UtilsService, formBuilderConfig: FormBuilderConfigService, dataSourceProvider: FormBuilderDataSourceProvider, formEmailAuthService: FormEmailAuthService, permissionResolver?: IPermissionResolver | undefined, permissionCache?: SharedPermissionCacheService | undefined);
|
|
24
|
+
protected cacheDependencies(): string[];
|
|
23
25
|
private ensureFormRepositoryInitialized;
|
|
24
|
-
private
|
|
26
|
+
private withSubmissionClaim;
|
|
25
27
|
private getActiveForm;
|
|
26
28
|
private applyCompanyFilterToQuery;
|
|
27
29
|
private static readonly PUBLICLY_SUBMITTABLE_ACCESS_TYPES;
|
|
28
30
|
private validateSubmissionAccess;
|
|
29
31
|
private resolveEmailVerifiedSubmitter;
|
|
32
|
+
private isSingleResponse;
|
|
30
33
|
private assertSingleResponseNotExceeded;
|
|
31
34
|
private buildUserDraftWhere;
|
|
32
35
|
getSelectQuery(query: SelectQueryBuilder<FormResult>, _user: ILoggedUserInfo | null, select?: string[]): Promise<{
|
|
@@ -41,10 +44,10 @@ export declare class FormResultService extends ApiService<CreateFormResultDto, U
|
|
|
41
44
|
protected convertEntityToResponseDto(entity: FormResult, _isRaw: boolean): IFormResult;
|
|
42
45
|
submitForm(dto: SubmitFormDto, user: ILoggedUserInfo | null, isPublic?: boolean): Promise<IFormResult>;
|
|
43
46
|
private persistSubmission;
|
|
47
|
+
private writeSubmission;
|
|
44
48
|
getMyDraft(formId: string, user: ILoggedUserInfo): Promise<IFormResult | null>;
|
|
45
49
|
updateDraft(draftId: string, dto: SubmitFormDto, user: ILoggedUserInfo): Promise<IFormResult>;
|
|
46
50
|
private publishSubmissionEvent;
|
|
47
|
-
private invalidateResultCache;
|
|
48
51
|
getByFormId(formId: string, user: ILoggedUserInfo | null, pagination?: {
|
|
49
52
|
page?: number;
|
|
50
53
|
pageSize?: number;
|
|
@@ -17,6 +17,7 @@ export declare class FormService extends ApiService<CreateFormDto, UpdateFormDto
|
|
|
17
17
|
private readonly permissionCache?;
|
|
18
18
|
constructor(cacheManager: HybridCache, utilsService: UtilsService, formBuilderConfig: FormBuilderConfigService, dataSourceProvider: FormBuilderDataSourceProvider, permissionResolver?: IPermissionResolver | undefined, permissionCache?: SharedPermissionCacheService | undefined);
|
|
19
19
|
convertSingleDtoToEntity(dto: CreateFormDto | UpdateFormDto, user: ILoggedUserInfo | null): Promise<Form>;
|
|
20
|
+
private assertValidComputedFields;
|
|
20
21
|
getSelectQuery(query: SelectQueryBuilder<Form>, _user: ILoggedUserInfo | null, select?: string[]): Promise<{
|
|
21
22
|
query: SelectQueryBuilder<Form>;
|
|
22
23
|
isRaw: boolean;
|
|
@@ -1,48 +1,24 @@
|
|
|
1
|
+
import type { Expression, IExpressionConditionGroup } from '@flusys/nestjs-shared/interfaces';
|
|
2
|
+
export type ComputedValueType = 'string' | 'number' | 'boolean';
|
|
1
3
|
export interface IComputedField {
|
|
2
4
|
id: string;
|
|
3
5
|
name: string;
|
|
4
6
|
key: string;
|
|
5
|
-
valueType:
|
|
7
|
+
valueType: ComputedValueType;
|
|
6
8
|
rules: IComputedRule[];
|
|
7
|
-
defaultValue?: string | number | null;
|
|
9
|
+
defaultValue?: string | number | boolean | null;
|
|
8
10
|
description?: string;
|
|
9
11
|
}
|
|
10
12
|
export interface IComputedRule {
|
|
11
13
|
id: string;
|
|
12
|
-
condition?:
|
|
13
|
-
|
|
14
|
+
condition?: IExpressionConditionGroup;
|
|
15
|
+
value: Expression;
|
|
14
16
|
}
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
export declare const COMPUTED_REF_PREFIX = "computed.";
|
|
18
|
+
export interface IComputedFieldIssue {
|
|
19
|
+
field: string;
|
|
20
|
+
code: string;
|
|
21
|
+
variables: Record<string, string | number>;
|
|
18
22
|
}
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
comparison: string;
|
|
22
|
-
value: unknown;
|
|
23
|
-
}
|
|
24
|
-
export interface IComputation {
|
|
25
|
-
type: ComputationType;
|
|
26
|
-
config: IDirectValueConfig | IFieldReferenceConfig | IArithmeticConfig;
|
|
27
|
-
}
|
|
28
|
-
export type ComputationType = 'direct' | 'field_reference' | 'arithmetic';
|
|
29
|
-
export interface IDirectValueConfig {
|
|
30
|
-
type: 'direct';
|
|
31
|
-
value: string | number;
|
|
32
|
-
}
|
|
33
|
-
export interface IFieldReferenceConfig {
|
|
34
|
-
type: 'field_reference';
|
|
35
|
-
fieldId: string;
|
|
36
|
-
}
|
|
37
|
-
export interface IArithmeticConfig {
|
|
38
|
-
type: 'arithmetic';
|
|
39
|
-
operation: ArithmeticOperation;
|
|
40
|
-
operands: IArithmeticOperand[];
|
|
41
|
-
}
|
|
42
|
-
export type ArithmeticOperation = 'sum' | 'subtract' | 'multiply' | 'divide' | 'average' | 'min' | 'max' | 'increment' | 'decrement';
|
|
43
|
-
export interface IArithmeticOperand {
|
|
44
|
-
type: 'field' | 'constant';
|
|
45
|
-
fieldId?: string;
|
|
46
|
-
value?: number;
|
|
47
|
-
}
|
|
48
|
-
export declare function calculateComputedFields(formData: Record<string, unknown>, computedFields: IComputedField[] | undefined): Record<string, unknown>;
|
|
23
|
+
export declare function calculateComputedFields(data: Record<string, unknown>, schema: unknown): Record<string, unknown>;
|
|
24
|
+
export declare function findComputedFieldIssue(schema: unknown): IComputedFieldIssue | null;
|