@flusys/nestjs-form-builder 9.1.0-rc.1 → 9.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -145,6 +145,23 @@ TypeOrmModule.forRoot({
145
145
 
146
146
  The service selects the correct entity automatically at request time — no manual switching needed.
147
147
 
148
+ ### ACTION_GROUP permission rule
149
+
150
+ An `ACTION_GROUP` form stores `permissionLogic` — an `ILogicNode` AND / OR tree from `@flusys/nestjs-shared/interfaces` (up to `MAX_PERMISSION_LOGIC_DEPTH` nested groups and `MAX_PERMISSION_LOGIC_CODES` codes), validated with `permissionLogicProblem()` from `@flusys/nestjs-shared/utils`:
151
+
152
+ ```typescript
153
+ {
154
+ type: 'group',
155
+ operator: 'AND',
156
+ children: [
157
+ { type: 'action', actionId: 'hr.survey.submit' },
158
+ { type: 'group', operator: 'OR', children: [{ type: 'action', actionId: 'hr.manager' }, { type: 'action', actionId: 'hr.admin' }] },
159
+ ],
160
+ }
161
+ ```
162
+
163
+ Fetching (`form/authenticated/:id`) and submitting check it with `evaluatePermissionLogic()` against the user's codes from `loadPermissionCodes()` (`PERMISSION_RESOLVER`, else `SharedPermissionCacheService`); when the codes cannot be read, access fails closed (403 `form.permission.check.failed`). A form with no rule is open to any logged-in user.
164
+
148
165
  ---
149
166
 
150
167
  ## 3. EMAIL_VERIFIED Access Type
@@ -1,4 +1,5 @@
1
1
  import { IdentityResponseDto } from '@flusys/nestjs-shared/dtos';
2
+ import { type ILogicNode } from '@flusys/nestjs-shared/interfaces';
2
3
  import { FormAccessType } from '../enums/form-access-type.enum.js';
3
4
  export declare class CreateFormDto {
4
5
  name: string;
@@ -6,7 +7,7 @@ export declare class CreateFormDto {
6
7
  slug?: string;
7
8
  schema: Record<string, unknown>;
8
9
  accessType?: FormAccessType;
9
- actionGroups?: string[];
10
+ permissionLogic?: ILogicNode | null;
10
11
  tags?: string[];
11
12
  companyId?: string;
12
13
  isActive?: boolean;
@@ -23,15 +24,15 @@ export declare class FormResponseDto extends IdentityResponseDto {
23
24
  schema: Record<string, unknown>;
24
25
  schemaVersion: number;
25
26
  accessType: FormAccessType;
26
- actionGroups: string[] | null;
27
+ permissionLogic: ILogicNode | null;
27
28
  tags: string[] | null;
28
29
  companyId: string | null;
29
30
  isActive: boolean;
30
31
  }
31
- declare const PublicFormResponseDto_base: import("@nestjs/common").Type<Pick<FormResponseDto, "description" | "name" | "schema" | "id" | "schemaVersion">>;
32
+ declare const PublicFormResponseDto_base: import("@nestjs/common").Type<Pick<FormResponseDto, "name" | "description" | "schema" | "id" | "schemaVersion">>;
32
33
  export declare class PublicFormResponseDto extends PublicFormResponseDto_base {
33
34
  }
34
- declare const FormAccessInfoResponseDto_base: import("@nestjs/common").Type<Pick<FormResponseDto, "description" | "name" | "accessType" | "actionGroups" | "isActive" | "id">>;
35
+ declare const FormAccessInfoResponseDto_base: import("@nestjs/common").Type<Pick<FormResponseDto, "name" | "description" | "accessType" | "permissionLogic" | "isActive" | "id">>;
35
36
  export declare class FormAccessInfoResponseDto extends FormAccessInfoResponseDto_base {
36
37
  }
37
38
  export declare class EmailVerifiedFormResponseDto extends PublicFormResponseDto {
@@ -1,4 +1,5 @@
1
1
  import { Identity } from '@flusys/nestjs-shared/entities';
2
+ import { type ILogicNode } from '@flusys/nestjs-shared/interfaces';
2
3
  import { FormAccessType } from '../enums/form-access-type.enum.js';
3
4
  export declare class Form extends Identity {
4
5
  name: string;
@@ -7,7 +8,7 @@ export declare class Form extends Identity {
7
8
  schema: Record<string, unknown>;
8
9
  schemaVersion: number;
9
10
  accessType: FormAccessType;
10
- actionGroups: string[] | null;
11
+ permissionLogic: ILogicNode | null;
11
12
  tags: string[] | null;
12
13
  isActive: boolean;
13
14
  }
package/fesm/432.js CHANGED
@@ -3,6 +3,7 @@ import { Identity as entities_Identity } from "@flusys/nestjs-shared/entities";
3
3
  import * as __rspack_external__flusys_nestjs_shared_classes_6076a329 from "@flusys/nestjs-shared/classes";
4
4
  import * as __rspack_external__flusys_nestjs_shared_decorators_c129b601 from "@flusys/nestjs-shared/decorators";
5
5
  import * as __rspack_external__flusys_nestjs_shared_dtos_c4585026 from "@flusys/nestjs-shared/dtos";
6
+ import * as __rspack_external__flusys_nestjs_shared_interfaces_413f2706 from "@flusys/nestjs-shared/interfaces";
6
7
  import * as __rspack_external__flusys_nestjs_shared_modules_24702bee from "@flusys/nestjs-shared/modules";
7
8
  import * as __rspack_external__flusys_nestjs_shared_utils_9125c1f1 from "@flusys/nestjs-shared/utils";
8
9
  import * as __rspack_external__nestjs_common_a04939da from "@nestjs/common";
@@ -379,7 +380,7 @@ class Form extends entities_Identity {
379
380
  schema;
380
381
  schemaVersion;
381
382
  accessType;
382
- actionGroups;
383
+ permissionLogic;
383
384
  tags;
384
385
  isActive;
385
386
  }
@@ -436,10 +437,10 @@ _ts_decorate([
436
437
  (0,external_typeorm_.Column)({
437
438
  type: getJsonColumnType(),
438
439
  nullable: true,
439
- name: 'action_groups'
440
+ name: 'permission_logic'
440
441
  }),
441
442
  _ts_metadata("design:type", Object)
442
- ], Form.prototype, "actionGroups", void 0);
443
+ ], Form.prototype, "permissionLogic", void 0);
443
444
  _ts_decorate([
444
445
  (0,external_typeorm_.Column)({
445
446
  type: getJsonColumnType(),
@@ -1168,19 +1169,20 @@ FormEmailAuthService = _ts_decorate([
1168
1169
  160(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1169
1170
  /* import */ var _flusys_nestjs_shared_classes__rspack_import_0 = __webpack_require__(937);
1170
1171
  /* import */ var _flusys_nestjs_shared_decorators__rspack_import_1 = __webpack_require__(265);
1171
- /* import */ var _flusys_nestjs_shared_modules__rspack_import_2 = __webpack_require__(236);
1172
- /* import */ var _nestjs_common__rspack_import_3 = __webpack_require__(157);
1173
- /* import */ var typeorm__rspack_import_4 = __webpack_require__(158);
1174
- /* import */ var _config_index_js__rspack_import_11 = __webpack_require__(769);
1175
- /* import */ var _config_index_js__rspack_import_12 = __webpack_require__(137);
1176
- /* import */ var _dtos_form_result_dto_js__rspack_import_5 = __webpack_require__(788);
1177
- /* import */ var _entities_index_js__rspack_import_6 = __webpack_require__(814);
1178
- /* import */ var _enums_form_access_type_enum_js__rspack_import_13 = __webpack_require__(376);
1179
- /* import */ var _utils_computed_field_utils_js__rspack_import_14 = __webpack_require__(801);
1180
- /* import */ var _utils_permission_utils_js__rspack_import_7 = __webpack_require__(988);
1181
- /* import */ var _form_builder_config_service_js__rspack_import_8 = __webpack_require__(781);
1182
- /* import */ var _form_builder_datasource_provider_js__rspack_import_9 = __webpack_require__(593);
1183
- /* import */ var _form_email_auth_service_js__rspack_import_10 = __webpack_require__(474);
1172
+ /* import */ var _flusys_nestjs_shared_interfaces__rspack_import_2 = __webpack_require__(103);
1173
+ /* import */ var _flusys_nestjs_shared_modules__rspack_import_3 = __webpack_require__(236);
1174
+ /* import */ var _nestjs_common__rspack_import_4 = __webpack_require__(157);
1175
+ /* import */ var typeorm__rspack_import_5 = __webpack_require__(158);
1176
+ /* import */ var _config_index_js__rspack_import_12 = __webpack_require__(769);
1177
+ /* import */ var _config_index_js__rspack_import_13 = __webpack_require__(137);
1178
+ /* import */ var _dtos_form_result_dto_js__rspack_import_6 = __webpack_require__(788);
1179
+ /* import */ var _entities_index_js__rspack_import_7 = __webpack_require__(814);
1180
+ /* import */ var _enums_form_access_type_enum_js__rspack_import_14 = __webpack_require__(376);
1181
+ /* import */ var _utils_computed_field_utils_js__rspack_import_15 = __webpack_require__(801);
1182
+ /* import */ var _utils_permission_utils_js__rspack_import_8 = __webpack_require__(988);
1183
+ /* import */ var _form_builder_config_service_js__rspack_import_9 = __webpack_require__(781);
1184
+ /* import */ var _form_builder_datasource_provider_js__rspack_import_10 = __webpack_require__(593);
1185
+ /* import */ var _form_email_auth_service_js__rspack_import_11 = __webpack_require__(474);
1184
1186
  function _ts_decorate(decorators, target, key, desc) {
1185
1187
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1186
1188
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -1218,12 +1220,15 @@ function _ts_param(paramIndex, decorator) {
1218
1220
 
1219
1221
 
1220
1222
 
1223
+
1221
1224
  class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.ApiService {
1222
1225
  cacheManager;
1223
1226
  utilsService;
1224
1227
  formBuilderConfig;
1225
1228
  dataSourceProvider;
1226
1229
  formEmailAuthService;
1230
+ permissionResolver;
1231
+ permissionCache;
1227
1232
  formRepository = null;
1228
1233
  // Serializes the single-response check-then-insert per (formId, email) within
1229
1234
  // this process, so a double-click/two-tab submission can't both pass the check
@@ -1231,13 +1236,13 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
1231
1236
  // per-instance lock would never be shared between the two racing requests.
1232
1237
  // Does not span multiple app instances; see withSubmissionLock().
1233
1238
  static submissionLocks = new Map();
1234
- constructor(cacheManager, utilsService, formBuilderConfig, dataSourceProvider, formEmailAuthService){
1235
- super(_config_index_js__rspack_import_11/* .FORM_BUILDER_EVENT_ENTITIES.FORM_RESULT */.eF.FORM_RESULT, cacheManager, utilsService, FormResultService.name, true, _config_index_js__rspack_import_11/* .FORM_BUILDER_EVENT_MODULE */.T$, dataSourceProvider, _entities_index_js__rspack_import_6/* .FormResult */.ez), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.dataSourceProvider = dataSourceProvider, this.formEmailAuthService = formEmailAuthService;
1239
+ constructor(cacheManager, utilsService, formBuilderConfig, dataSourceProvider, formEmailAuthService, permissionResolver, permissionCache){
1240
+ super(_config_index_js__rspack_import_12/* .FORM_BUILDER_EVENT_ENTITIES.FORM_RESULT */.eF.FORM_RESULT, cacheManager, utilsService, FormResultService.name, true, _config_index_js__rspack_import_12/* .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;
1236
1241
  }
1237
1242
  async ensureFormRepositoryInitialized() {
1238
1243
  if (!this.formRepository) {
1239
1244
  const enableCompanyFeature = this.formBuilderConfig.isCompanyFeatureEnabled();
1240
- const formEntity = enableCompanyFeature ? _entities_index_js__rspack_import_6/* .FormWithCompany */.Wl : _entities_index_js__rspack_import_6/* .Form */.lV;
1245
+ const formEntity = enableCompanyFeature ? _entities_index_js__rspack_import_7/* .FormWithCompany */.Wl : _entities_index_js__rspack_import_7/* .Form */.lV;
1241
1246
  this.formRepository = await this.dataSourceProvider.getRepository(formEntity);
1242
1247
  }
1243
1248
  }
@@ -1264,13 +1269,13 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
1264
1269
  where: {
1265
1270
  id: formId,
1266
1271
  isActive: true,
1267
- deletedAt: (0,typeorm__rspack_import_4.IsNull)()
1272
+ deletedAt: (0,typeorm__rspack_import_5.IsNull)()
1268
1273
  }
1269
1274
  });
1270
1275
  if (!form) {
1271
- throw new _nestjs_common__rspack_import_3.NotFoundException({
1276
+ throw new _nestjs_common__rspack_import_4.NotFoundException({
1272
1277
  message: 'Form not found or inactive',
1273
- messageKey: _config_index_js__rspack_import_12/* .FORM_MESSAGES.NOT_FOUND */.Q.NOT_FOUND
1278
+ messageKey: _config_index_js__rspack_import_13/* .FORM_MESSAGES.NOT_FOUND */.Q.NOT_FOUND
1274
1279
  });
1275
1280
  }
1276
1281
  return form;
@@ -1283,64 +1288,67 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
1283
1288
  }
1284
1289
  }
1285
1290
  static PUBLICLY_SUBMITTABLE_ACCESS_TYPES = new Set([
1286
- _enums_form_access_type_enum_js__rspack_import_13/* .FormAccessType.PUBLIC */.h.PUBLIC,
1287
- _enums_form_access_type_enum_js__rspack_import_13/* .FormAccessType.EMAIL_VERIFIED */.h.EMAIL_VERIFIED
1291
+ _enums_form_access_type_enum_js__rspack_import_14/* .FormAccessType.PUBLIC */.h.PUBLIC,
1292
+ _enums_form_access_type_enum_js__rspack_import_14/* .FormAccessType.EMAIL_VERIFIED */.h.EMAIL_VERIFIED
1288
1293
  ]);
1289
1294
  async validateSubmissionAccess(form, user, isPublic) {
1290
1295
  if (isPublic) {
1291
1296
  if (!FormResultService.PUBLICLY_SUBMITTABLE_ACCESS_TYPES.has(form.accessType)) {
1292
- throw new _nestjs_common__rspack_import_3.UnauthorizedException({
1297
+ throw new _nestjs_common__rspack_import_4.UnauthorizedException({
1293
1298
  message: 'This form is not available for public submission',
1294
- messageKey: _config_index_js__rspack_import_12/* .FORM_MESSAGES.NOT_PUBLIC */.Q.NOT_PUBLIC
1299
+ messageKey: _config_index_js__rspack_import_13/* .FORM_MESSAGES.NOT_PUBLIC */.Q.NOT_PUBLIC
1295
1300
  });
1296
1301
  }
1297
1302
  return;
1298
1303
  }
1299
1304
  switch(form.accessType){
1300
- case _enums_form_access_type_enum_js__rspack_import_13/* .FormAccessType.PUBLIC */.h.PUBLIC:
1305
+ case _enums_form_access_type_enum_js__rspack_import_14/* .FormAccessType.PUBLIC */.h.PUBLIC:
1301
1306
  return;
1302
- case _enums_form_access_type_enum_js__rspack_import_13/* .FormAccessType.EMAIL_VERIFIED */.h.EMAIL_VERIFIED:
1303
- throw new _nestjs_common__rspack_import_3.BadRequestException({
1307
+ case _enums_form_access_type_enum_js__rspack_import_14/* .FormAccessType.EMAIL_VERIFIED */.h.EMAIL_VERIFIED:
1308
+ throw new _nestjs_common__rspack_import_4.BadRequestException({
1304
1309
  message: 'Email-verified forms must be submitted through the public endpoint',
1305
- messageKey: _config_index_js__rspack_import_12/* .FORM_MESSAGES.INVALID_ACCESS_TYPE */.Q.INVALID_ACCESS_TYPE
1310
+ messageKey: _config_index_js__rspack_import_13/* .FORM_MESSAGES.INVALID_ACCESS_TYPE */.Q.INVALID_ACCESS_TYPE
1306
1311
  });
1307
- case _enums_form_access_type_enum_js__rspack_import_13/* .FormAccessType.ACTION_GROUP */.h.ACTION_GROUP:
1308
- case _enums_form_access_type_enum_js__rspack_import_13/* .FormAccessType.AUTHENTICATED */.h.AUTHENTICATED:
1312
+ case _enums_form_access_type_enum_js__rspack_import_14/* .FormAccessType.ACTION_GROUP */.h.ACTION_GROUP:
1313
+ case _enums_form_access_type_enum_js__rspack_import_14/* .FormAccessType.AUTHENTICATED */.h.AUTHENTICATED:
1309
1314
  break;
1310
1315
  default:
1311
- throw new _nestjs_common__rspack_import_3.BadRequestException({
1316
+ throw new _nestjs_common__rspack_import_4.BadRequestException({
1312
1317
  message: 'Invalid form access type',
1313
- messageKey: _config_index_js__rspack_import_12/* .FORM_MESSAGES.INVALID_ACCESS_TYPE */.Q.INVALID_ACCESS_TYPE
1318
+ messageKey: _config_index_js__rspack_import_13/* .FORM_MESSAGES.INVALID_ACCESS_TYPE */.Q.INVALID_ACCESS_TYPE
1314
1319
  });
1315
1320
  }
1316
1321
  if (!user) {
1317
- throw new _nestjs_common__rspack_import_3.UnauthorizedException({
1322
+ throw new _nestjs_common__rspack_import_4.UnauthorizedException({
1318
1323
  message: 'Authentication required to submit this form',
1319
- messageKey: _config_index_js__rspack_import_12/* .FORM_MESSAGES.AUTH_REQUIRED */.Q.AUTH_REQUIRED
1324
+ messageKey: _config_index_js__rspack_import_13/* .FORM_MESSAGES.AUTH_REQUIRED */.Q.AUTH_REQUIRED
1320
1325
  });
1321
1326
  }
1322
- if (form.accessType === _enums_form_access_type_enum_js__rspack_import_13/* .FormAccessType.ACTION_GROUP */.h.ACTION_GROUP && form.actionGroups?.length) {
1323
- const hasPermission = await (0,_utils_permission_utils_js__rspack_import_7/* .validateUserPermissions */.i)(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled());
1324
- if (!hasPermission) {
1325
- throw new _nestjs_common__rspack_import_3.ForbiddenException({
1327
+ if (form.accessType === _enums_form_access_type_enum_js__rspack_import_14/* .FormAccessType.ACTION_GROUP */.h.ACTION_GROUP && form.permissionLogic) {
1328
+ const permitted = await (0,_utils_permission_utils_js__rspack_import_8/* .hasFormPermission */.E)(user, form.permissionLogic, {
1329
+ resolver: this.permissionResolver,
1330
+ cache: this.permissionCache
1331
+ });
1332
+ if (!permitted) {
1333
+ throw new _nestjs_common__rspack_import_4.ForbiddenException({
1326
1334
  message: 'You do not have permission to submit this form',
1327
- messageKey: _config_index_js__rspack_import_12/* .FORM_MESSAGES.ACCESS_DENIED */.Q.ACCESS_DENIED
1335
+ messageKey: _config_index_js__rspack_import_13/* .FORM_MESSAGES.ACCESS_DENIED */.Q.ACCESS_DENIED
1328
1336
  });
1329
1337
  }
1330
- } else if (form.accessType !== _enums_form_access_type_enum_js__rspack_import_13/* .FormAccessType.AUTHENTICATED */.h.AUTHENTICATED) {
1331
- throw new _nestjs_common__rspack_import_3.BadRequestException({
1338
+ } else if (form.accessType !== _enums_form_access_type_enum_js__rspack_import_14/* .FormAccessType.AUTHENTICATED */.h.AUTHENTICATED) {
1339
+ throw new _nestjs_common__rspack_import_4.BadRequestException({
1332
1340
  message: 'Invalid form access type',
1333
- messageKey: _config_index_js__rspack_import_12/* .FORM_MESSAGES.INVALID_ACCESS_TYPE */.Q.INVALID_ACCESS_TYPE
1341
+ messageKey: _config_index_js__rspack_import_13/* .FORM_MESSAGES.INVALID_ACCESS_TYPE */.Q.INVALID_ACCESS_TYPE
1334
1342
  });
1335
1343
  }
1336
1344
  }
1337
1345
  async resolveEmailVerifiedSubmitter(form, user, accessToken) {
1338
- if (form.accessType !== _enums_form_access_type_enum_js__rspack_import_13/* .FormAccessType.EMAIL_VERIFIED */.h.EMAIL_VERIFIED) return null;
1346
+ if (form.accessType !== _enums_form_access_type_enum_js__rspack_import_14/* .FormAccessType.EMAIL_VERIFIED */.h.EMAIL_VERIFIED) return null;
1339
1347
  const email = await this.formEmailAuthService.resolveSubmitterEmail(form.id, user ?? undefined, accessToken);
1340
1348
  if (!email) {
1341
- throw new _nestjs_common__rspack_import_3.UnauthorizedException({
1349
+ throw new _nestjs_common__rspack_import_4.UnauthorizedException({
1342
1350
  message: 'Email verification required to submit this form',
1343
- messageKey: _config_index_js__rspack_import_12/* .FORM_MESSAGES.EMAIL_VERIFICATION_REQUIRED */.Q.EMAIL_VERIFICATION_REQUIRED
1351
+ messageKey: _config_index_js__rspack_import_13/* .FORM_MESSAGES.EMAIL_VERIFICATION_REQUIRED */.Q.EMAIL_VERIFICATION_REQUIRED
1344
1352
  });
1345
1353
  }
1346
1354
  return email;
@@ -1354,13 +1362,13 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
1354
1362
  formId: form.id,
1355
1363
  submitterEmail,
1356
1364
  isDraft: false,
1357
- deletedAt: (0,typeorm__rspack_import_4.IsNull)()
1365
+ deletedAt: (0,typeorm__rspack_import_5.IsNull)()
1358
1366
  }
1359
1367
  });
1360
1368
  if (count > 0) {
1361
- throw new _nestjs_common__rspack_import_3.ConflictException({
1369
+ throw new _nestjs_common__rspack_import_4.ConflictException({
1362
1370
  message: 'You have already submitted a response to this form',
1363
- messageKey: _config_index_js__rspack_import_12/* .FORM_RESULT_MESSAGES.ALREADY_SUBMITTED */.y.ALREADY_SUBMITTED
1371
+ messageKey: _config_index_js__rspack_import_13/* .FORM_RESULT_MESSAGES.ALREADY_SUBMITTED */.y.ALREADY_SUBMITTED
1364
1372
  });
1365
1373
  }
1366
1374
  }
@@ -1369,7 +1377,7 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
1369
1377
  formId,
1370
1378
  submittedById: userId,
1371
1379
  isDraft: true,
1372
- deletedAt: (0,typeorm__rspack_import_4.IsNull)()
1380
+ deletedAt: (0,typeorm__rspack_import_5.IsNull)()
1373
1381
  };
1374
1382
  }
1375
1383
  // Query Customization
@@ -1411,7 +1419,7 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
1411
1419
  if (!computedFields || computedFields.length === 0) {
1412
1420
  return data;
1413
1421
  }
1414
- const computedValues = (0,_utils_computed_field_utils_js__rspack_import_14/* .calculateComputedFields */.X)(data, computedFields);
1422
+ const computedValues = (0,_utils_computed_field_utils_js__rspack_import_15/* .calculateComputedFields */.X)(data, computedFields);
1415
1423
  return {
1416
1424
  ...data,
1417
1425
  _computed: computedValues
@@ -1514,13 +1522,13 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
1514
1522
  where: {
1515
1523
  id: draftId,
1516
1524
  submittedById: user.id,
1517
- deletedAt: (0,typeorm__rspack_import_4.IsNull)()
1525
+ deletedAt: (0,typeorm__rspack_import_5.IsNull)()
1518
1526
  }
1519
1527
  });
1520
1528
  if (!draft) {
1521
- throw new _nestjs_common__rspack_import_3.NotFoundException({
1529
+ throw new _nestjs_common__rspack_import_4.NotFoundException({
1522
1530
  message: 'Draft not found',
1523
- messageKey: _config_index_js__rspack_import_12/* .FORM_RESULT_MESSAGES.NOT_FOUND */.y.NOT_FOUND
1531
+ messageKey: _config_index_js__rspack_import_13/* .FORM_RESULT_MESSAGES.NOT_FOUND */.y.NOT_FOUND
1524
1532
  });
1525
1533
  }
1526
1534
  const form = await this.getActiveForm(dto.formId);
@@ -1540,7 +1548,7 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
1540
1548
  return this.convertEntityToResponseDto(saved, false);
1541
1549
  }
1542
1550
  async publishSubmissionEvent(result, formId, isDraft, user) {
1543
- await this.publishDomainAction(isDraft ? _config_index_js__rspack_import_11/* .FORM_BUILDER_EVENT_ACTIONS.DRAFT_SAVED */.ov.DRAFT_SAVED : _config_index_js__rspack_import_11/* .FORM_BUILDER_EVENT_ACTIONS.SUBMITTED */.ov.SUBMITTED, {
1551
+ await this.publishDomainAction(isDraft ? _config_index_js__rspack_import_12/* .FORM_BUILDER_EVENT_ACTIONS.DRAFT_SAVED */.ov.DRAFT_SAVED : _config_index_js__rspack_import_12/* .FORM_BUILDER_EVENT_ACTIONS.SUBMITTED */.ov.SUBMITTED, {
1544
1552
  ids: [
1545
1553
  result.id
1546
1554
  ],
@@ -1581,7 +1589,7 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
1581
1589
  formId,
1582
1590
  submittedById: user.id,
1583
1591
  isDraft: false,
1584
- deletedAt: (0,typeorm__rspack_import_4.IsNull)()
1592
+ deletedAt: (0,typeorm__rspack_import_5.IsNull)()
1585
1593
  }
1586
1594
  });
1587
1595
  return count > 0;
@@ -1595,7 +1603,7 @@ class FormResultService extends _flusys_nestjs_shared_classes__rspack_import_0.A
1595
1603
  formId,
1596
1604
  submitterEmail: email,
1597
1605
  isDraft: false,
1598
- deletedAt: (0,typeorm__rspack_import_4.IsNull)()
1606
+ deletedAt: (0,typeorm__rspack_import_5.IsNull)()
1599
1607
  }
1600
1608
  });
1601
1609
  return count > 0;
@@ -1608,7 +1616,7 @@ _ts_decorate([
1608
1616
  }),
1609
1617
  _ts_metadata("design:type", Function),
1610
1618
  _ts_metadata("design:paramtypes", [
1611
- typeof _dtos_form_result_dto_js__rspack_import_5/* .SubmitFormDto */.sH === "undefined" ? Object : _dtos_form_result_dto_js__rspack_import_5/* .SubmitFormDto */.sH,
1619
+ typeof _dtos_form_result_dto_js__rspack_import_6/* .SubmitFormDto */.sH === "undefined" ? Object : _dtos_form_result_dto_js__rspack_import_6/* .SubmitFormDto */.sH,
1612
1620
  Object,
1613
1621
  void 0
1614
1622
  ]),
@@ -1622,27 +1630,33 @@ _ts_decorate([
1622
1630
  _ts_metadata("design:type", Function),
1623
1631
  _ts_metadata("design:paramtypes", [
1624
1632
  String,
1625
- typeof _dtos_form_result_dto_js__rspack_import_5/* .SubmitFormDto */.sH === "undefined" ? Object : _dtos_form_result_dto_js__rspack_import_5/* .SubmitFormDto */.sH,
1633
+ typeof _dtos_form_result_dto_js__rspack_import_6/* .SubmitFormDto */.sH === "undefined" ? Object : _dtos_form_result_dto_js__rspack_import_6/* .SubmitFormDto */.sH,
1626
1634
  typeof ILoggedUserInfo === "undefined" ? Object : ILoggedUserInfo
1627
1635
  ]),
1628
1636
  _ts_metadata("design:returntype", Promise)
1629
1637
  ], FormResultService.prototype, "updateDraft", null);
1630
1638
  FormResultService = _ts_decorate([
1631
- (0,_nestjs_common__rspack_import_3.Injectable)({
1632
- scope: _nestjs_common__rspack_import_3.Scope.REQUEST
1639
+ (0,_nestjs_common__rspack_import_4.Injectable)({
1640
+ scope: _nestjs_common__rspack_import_4.Scope.REQUEST
1633
1641
  }),
1634
- _ts_param(0, (0,_nestjs_common__rspack_import_3.Inject)('CACHE_INSTANCE')),
1635
- _ts_param(1, (0,_nestjs_common__rspack_import_3.Inject)(_flusys_nestjs_shared_modules__rspack_import_2.UtilsService)),
1636
- _ts_param(2, (0,_nestjs_common__rspack_import_3.Inject)(_form_builder_config_service_js__rspack_import_8/* .FormBuilderConfigService */.j)),
1637
- _ts_param(3, (0,_nestjs_common__rspack_import_3.Inject)(_form_builder_datasource_provider_js__rspack_import_9/* .FormBuilderDataSourceProvider */._)),
1638
- _ts_param(4, (0,_nestjs_common__rspack_import_3.Inject)(_form_email_auth_service_js__rspack_import_10/* .FormEmailAuthService */.w)),
1642
+ _ts_param(0, (0,_nestjs_common__rspack_import_4.Inject)('CACHE_INSTANCE')),
1643
+ _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)(_form_builder_config_service_js__rspack_import_9/* .FormBuilderConfigService */.j)),
1645
+ _ts_param(3, (0,_nestjs_common__rspack_import_4.Inject)(_form_builder_datasource_provider_js__rspack_import_10/* .FormBuilderDataSourceProvider */._)),
1646
+ _ts_param(4, (0,_nestjs_common__rspack_import_4.Inject)(_form_email_auth_service_js__rspack_import_11/* .FormEmailAuthService */.w)),
1647
+ _ts_param(5, (0,_nestjs_common__rspack_import_4.Optional)()),
1648
+ _ts_param(5, (0,_nestjs_common__rspack_import_4.Inject)(_flusys_nestjs_shared_interfaces__rspack_import_2.PERMISSION_RESOLVER)),
1649
+ _ts_param(6, (0,_nestjs_common__rspack_import_4.Optional)()),
1650
+ _ts_param(6, (0,_nestjs_common__rspack_import_4.Inject)(_flusys_nestjs_shared_modules__rspack_import_3.SharedPermissionCacheService)),
1639
1651
  _ts_metadata("design:type", Function),
1640
1652
  _ts_metadata("design:paramtypes", [
1641
1653
  typeof _flusys_nestjs_shared_classes__rspack_import_0.HybridCache === "undefined" ? Object : _flusys_nestjs_shared_classes__rspack_import_0.HybridCache,
1642
- typeof _flusys_nestjs_shared_modules__rspack_import_2.UtilsService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_2.UtilsService,
1643
- typeof _form_builder_config_service_js__rspack_import_8/* .FormBuilderConfigService */.j === "undefined" ? Object : _form_builder_config_service_js__rspack_import_8/* .FormBuilderConfigService */.j,
1644
- typeof _form_builder_datasource_provider_js__rspack_import_9/* .FormBuilderDataSourceProvider */._ === "undefined" ? Object : _form_builder_datasource_provider_js__rspack_import_9/* .FormBuilderDataSourceProvider */._,
1645
- typeof _form_email_auth_service_js__rspack_import_10/* .FormEmailAuthService */.w === "undefined" ? Object : _form_email_auth_service_js__rspack_import_10/* .FormEmailAuthService */.w
1654
+ typeof _flusys_nestjs_shared_modules__rspack_import_3.UtilsService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_3.UtilsService,
1655
+ typeof _form_builder_config_service_js__rspack_import_9/* .FormBuilderConfigService */.j === "undefined" ? Object : _form_builder_config_service_js__rspack_import_9/* .FormBuilderConfigService */.j,
1656
+ typeof _form_builder_datasource_provider_js__rspack_import_10/* .FormBuilderDataSourceProvider */._ === "undefined" ? Object : _form_builder_datasource_provider_js__rspack_import_10/* .FormBuilderDataSourceProvider */._,
1657
+ typeof _form_email_auth_service_js__rspack_import_11/* .FormEmailAuthService */.w === "undefined" ? Object : _form_email_auth_service_js__rspack_import_11/* .FormEmailAuthService */.w,
1658
+ typeof IPermissionResolver === "undefined" ? Object : IPermissionResolver,
1659
+ typeof _flusys_nestjs_shared_modules__rspack_import_3.SharedPermissionCacheService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_3.SharedPermissionCacheService
1646
1660
  ])
1647
1661
  ], FormResultService);
1648
1662
 
@@ -1663,6 +1677,8 @@ __webpack_require__.d(__webpack_exports__, {
1663
1677
 
1664
1678
  // EXTERNAL MODULE: external "@flusys/nestjs-shared/classes"
1665
1679
  var classes_ = __webpack_require__(937);
1680
+ // EXTERNAL MODULE: external "@flusys/nestjs-shared/interfaces"
1681
+ var interfaces_ = __webpack_require__(103);
1666
1682
  // EXTERNAL MODULE: external "@flusys/nestjs-shared/modules"
1667
1683
  var modules_ = __webpack_require__(236);
1668
1684
  // EXTERNAL MODULE: external "@flusys/nestjs-shared/utils"
@@ -1721,12 +1737,15 @@ function _ts_param(paramIndex, decorator) {
1721
1737
 
1722
1738
 
1723
1739
 
1740
+
1724
1741
  class FormService extends classes_.ApiService {
1725
1742
  cacheManager;
1726
1743
  utilsService;
1727
1744
  formBuilderConfig;
1728
- constructor(cacheManager, utilsService, formBuilderConfig, dataSourceProvider){
1729
- super(event_actions/* .FORM_BUILDER_EVENT_ENTITIES.FORM */.eF.FORM, cacheManager, utilsService, FormService.name, true, event_actions/* .FORM_BUILDER_EVENT_MODULE */.T$, dataSourceProvider, formBuilderConfig.isCompanyFeatureEnabled() ? entities/* .FormWithCompany */.Wl : entities/* .Form */.lV), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig;
1745
+ permissionResolver;
1746
+ permissionCache;
1747
+ constructor(cacheManager, utilsService, formBuilderConfig, dataSourceProvider, permissionResolver, permissionCache){
1748
+ super(event_actions/* .FORM_BUILDER_EVENT_ENTITIES.FORM */.eF.FORM, cacheManager, utilsService, FormService.name, true, event_actions/* .FORM_BUILDER_EVENT_MODULE */.T$, dataSourceProvider, formBuilderConfig.isCompanyFeatureEnabled() ? entities/* .FormWithCompany */.Wl : entities/* .Form */.lV), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.permissionResolver = permissionResolver, this.permissionCache = permissionCache;
1730
1749
  }
1731
1750
  // Entity Conversion
1732
1751
  async convertSingleDtoToEntity(dto, user) {
@@ -1786,7 +1805,7 @@ class FormService extends classes_.ApiService {
1786
1805
  'schema',
1787
1806
  'schemaVersion',
1788
1807
  'accessType',
1789
- 'actionGroups',
1808
+ 'permissionLogic',
1790
1809
  'tags',
1791
1810
  'isActive',
1792
1811
  'createdAt',
@@ -1856,7 +1875,7 @@ class FormService extends classes_.ApiService {
1856
1875
  schema: entity.schema,
1857
1876
  schemaVersion: entity.schemaVersion,
1858
1877
  accessType: entity.accessType,
1859
- actionGroups: entity.actionGroups,
1878
+ permissionLogic: entity.permissionLogic,
1860
1879
  tags: entity.tags,
1861
1880
  isActive: entity.isActive,
1862
1881
  companyId: entityWithCompany.companyId ?? null,
@@ -1966,7 +1985,7 @@ class FormService extends classes_.ApiService {
1966
1985
  'name',
1967
1986
  'description',
1968
1987
  'accessType',
1969
- 'actionGroups',
1988
+ 'permissionLogic',
1970
1989
  'isActive'
1971
1990
  ]
1972
1991
  });
@@ -1981,7 +2000,7 @@ class FormService extends classes_.ApiService {
1981
2000
  name: form.name,
1982
2001
  description: form.description,
1983
2002
  accessType: form.accessType,
1984
- actionGroups: form.actionGroups,
2003
+ permissionLogic: form.permissionLogic,
1985
2004
  isActive: form.isActive
1986
2005
  };
1987
2006
  }
@@ -2014,10 +2033,12 @@ class FormService extends classes_.ApiService {
2014
2033
  }
2015
2034
  async getAuthenticatedForm(formId, user) {
2016
2035
  const form = await this.getFormForSubmission(formId, user);
2017
- // For action_group access, check permissions from cache
2018
- if (form.accessType === form_access_type_enum/* .FormAccessType.ACTION_GROUP */.h.ACTION_GROUP && form.actionGroups?.length) {
2019
- const hasPermission = await (0,permission_utils/* .validateUserPermissions */.i)(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled());
2020
- if (!hasPermission) {
2036
+ if (form.accessType === form_access_type_enum/* .FormAccessType.ACTION_GROUP */.h.ACTION_GROUP && form.permissionLogic) {
2037
+ const permitted = await (0,permission_utils/* .hasFormPermission */.E)(user, form.permissionLogic, {
2038
+ resolver: this.permissionResolver,
2039
+ cache: this.permissionCache
2040
+ });
2041
+ if (!permitted) {
2021
2042
  throw new common_.ForbiddenException({
2022
2043
  message: 'You do not have permission to access this form',
2023
2044
  messageKey: message_keys/* .FORM_MESSAGES.ACCESS_DENIED */.Q.ACCESS_DENIED
@@ -2035,12 +2056,18 @@ FormService = _ts_decorate([
2035
2056
  _ts_param(1, (0,common_.Inject)(modules_.UtilsService)),
2036
2057
  _ts_param(2, (0,common_.Inject)(form_builder_config_service/* .FormBuilderConfigService */.j)),
2037
2058
  _ts_param(3, (0,common_.Inject)(form_builder_datasource_provider/* .FormBuilderDataSourceProvider */._)),
2059
+ _ts_param(4, (0,common_.Optional)()),
2060
+ _ts_param(4, (0,common_.Inject)(interfaces_.PERMISSION_RESOLVER)),
2061
+ _ts_param(5, (0,common_.Optional)()),
2062
+ _ts_param(5, (0,common_.Inject)(modules_.SharedPermissionCacheService)),
2038
2063
  _ts_metadata("design:type", Function),
2039
2064
  _ts_metadata("design:paramtypes", [
2040
2065
  typeof classes_.HybridCache === "undefined" ? Object : classes_.HybridCache,
2041
2066
  typeof modules_.UtilsService === "undefined" ? Object : modules_.UtilsService,
2042
2067
  typeof form_builder_config_service/* .FormBuilderConfigService */.j === "undefined" ? Object : form_builder_config_service/* .FormBuilderConfigService */.j,
2043
- typeof form_builder_datasource_provider/* .FormBuilderDataSourceProvider */._ === "undefined" ? Object : form_builder_datasource_provider/* .FormBuilderDataSourceProvider */._
2068
+ typeof form_builder_datasource_provider/* .FormBuilderDataSourceProvider */._ === "undefined" ? Object : form_builder_datasource_provider/* .FormBuilderDataSourceProvider */._,
2069
+ typeof IPermissionResolver === "undefined" ? Object : IPermissionResolver,
2070
+ typeof modules_.SharedPermissionCacheService === "undefined" ? Object : modules_.SharedPermissionCacheService
2044
2071
  ])
2045
2072
  ], FormService);
2046
2073
 
@@ -2250,53 +2277,31 @@ __webpack_require__.d(__webpack_exports__, {
2250
2277
 
2251
2278
  },
2252
2279
  988(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
2253
- /* import */ var _config_index_js__rspack_import_1 = __webpack_require__(137);
2254
- /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(157);
2280
+ /* import */ var _flusys_nestjs_shared_utils__rspack_import_0 = __webpack_require__(216);
2281
+ /* import */ var _nestjs_common__rspack_import_1 = __webpack_require__(157);
2282
+ /* import */ var _config_index_js__rspack_import_2 = __webpack_require__(137);
2255
2283
 
2256
2284
 
2257
- const CACHE_PREFIX = 'permissions';
2258
- /**
2259
- * Build permission cache key for a user
2260
- * Uses same cache key format as PermissionGuard in nestjs-shared
2261
- */ function buildPermissionCacheKey(user, enableCompanyFeature) {
2262
- if (enableCompanyFeature && user.companyId) {
2263
- return `${CACHE_PREFIX}:company:${user.companyId}:branch:${user.branchId || 'null'}:user:${user.id}`;
2264
- }
2265
- return `${CACHE_PREFIX}:user:${user.id}`;
2266
- }
2267
- /**
2268
- * Get user permissions from cache
2269
- * Fail-closed: if cache fails, deny access rather than allowing potentially unauthorized access
2270
- */ async function getUserPermissionsFromCache(user, cacheManager, enableCompanyFeature) {
2271
- const cacheKey = buildPermissionCacheKey(user, enableCompanyFeature);
2285
+
2286
+ /** Whether `user` passes a form's AND / OR permission rule; fails closed when permissions cannot be read. */ async function hasFormPermission(user, logic, sources) {
2287
+ let granted;
2272
2288
  try {
2273
- const permissions = await cacheManager.get(cacheKey);
2274
- return permissions ?? [];
2289
+ granted = await (0,_flusys_nestjs_shared_utils__rspack_import_0.loadPermissionCodes)({
2290
+ userId: user.id,
2291
+ companyId: user.companyId,
2292
+ branchId: user.branchId
2293
+ }, sources);
2275
2294
  } catch {
2276
- throw new _nestjs_common__rspack_import_0.ForbiddenException({
2295
+ throw new _nestjs_common__rspack_import_1.ForbiddenException({
2277
2296
  message: 'Unable to verify permissions. Please try again.',
2278
- messageKey: _config_index_js__rspack_import_1/* .FORM_MESSAGES.PERMISSION_CHECK_FAILED */.Q.PERMISSION_CHECK_FAILED
2297
+ messageKey: _config_index_js__rspack_import_2/* .FORM_MESSAGES.PERMISSION_CHECK_FAILED */.Q.PERMISSION_CHECK_FAILED
2279
2298
  });
2280
2299
  }
2281
- }
2282
- /**
2283
- * Validate user has at least one of the required permissions
2284
- *
2285
- * @param user - The logged-in user
2286
- * @param requiredPermissions - Array of required permission strings (user needs at least one)
2287
- * @param cacheManager - Cache manager instance
2288
- * @param enableCompanyFeature - Whether company feature is enabled
2289
- * @returns true if user has permission, false otherwise
2290
- */ async function validateUserPermissions(user, requiredPermissions, cacheManager, enableCompanyFeature) {
2291
- if (!requiredPermissions.length) {
2292
- return true;
2293
- }
2294
- const userPermissions = await getUserPermissionsFromCache(user, cacheManager, enableCompanyFeature);
2295
- return requiredPermissions.some((perm)=>userPermissions.includes(perm));
2300
+ return (0,_flusys_nestjs_shared_utils__rspack_import_0.evaluatePermissionLogic)(logic, granted).passed;
2296
2301
  }
2297
2302
 
2298
2303
  __webpack_require__.d(__webpack_exports__, {
2299
- i: () => (validateUserPermissions)
2304
+ E: () => (hasFormPermission)
2300
2305
  });
2301
2306
 
2302
2307
 
@@ -2318,6 +2323,12 @@ module.exports = __rspack_external__flusys_nestjs_shared_decorators_c129b601;
2318
2323
  module.exports = __rspack_external__flusys_nestjs_shared_dtos_c4585026;
2319
2324
 
2320
2325
 
2326
+ },
2327
+ 103(module) {
2328
+
2329
+ module.exports = __rspack_external__flusys_nestjs_shared_interfaces_413f2706;
2330
+
2331
+
2321
2332
  },
2322
2333
  236(module) {
2323
2334
 
package/fesm/720.js CHANGED
@@ -733,6 +733,8 @@ __webpack_require__.d(__webpack_exports__, {
733
733
 
734
734
  // EXTERNAL MODULE: external "@flusys/nestjs-shared/dtos"
735
735
  var dtos_ = __webpack_require__(873);
736
+ // EXTERNAL MODULE: external "@flusys/nestjs-shared/utils"
737
+ var utils_ = __webpack_require__(216);
736
738
  // EXTERNAL MODULE: external "@nestjs/swagger"
737
739
  var swagger_ = __webpack_require__(176);
738
740
  // EXTERNAL MODULE: external "class-transformer"
@@ -765,13 +767,23 @@ function _ts_metadata(metadataKey, metadataValue) {
765
767
 
766
768
 
767
769
 
770
+
771
+ function IsPermissionLogic() {
772
+ return (0,external_class_validator_.ValidateBy)({
773
+ name: 'isPermissionLogic',
774
+ validator: {
775
+ validate: (value)=>(0,utils_.permissionLogicProblem)(value) === null,
776
+ defaultMessage: ()=>`$property must be a non-empty AND / OR permission rule with at most ${utils_.MAX_PERMISSION_LOGIC_CODES} permissions`
777
+ }
778
+ });
779
+ }
768
780
  class CreateFormDto {
769
781
  name;
770
782
  description;
771
783
  slug;
772
784
  schema;
773
785
  accessType;
774
- actionGroups;
786
+ permissionLogic;
775
787
  tags;
776
788
  companyId;
777
789
  isActive;
@@ -833,20 +845,28 @@ _ts_decorate([
833
845
  ], CreateFormDto.prototype, "accessType", void 0);
834
846
  _ts_decorate([
835
847
  (0,swagger_.ApiProperty)({
836
- description: 'Action groups for permission-based access (only used when accessType is action_group)',
837
- example: [
838
- 'hr.survey.submit',
839
- 'employee.feedback.submit'
840
- ],
841
- required: false
842
- }),
843
- (0,external_class_validator_.IsArray)(),
844
- (0,external_class_validator_.IsString)({
845
- each: true
848
+ description: 'AND / OR permission rule a user must pass (only used when accessType is action_group)',
849
+ example: {
850
+ type: 'group',
851
+ operator: 'OR',
852
+ children: [
853
+ {
854
+ type: 'action',
855
+ actionId: 'hr.survey.submit'
856
+ },
857
+ {
858
+ type: 'action',
859
+ actionId: 'employee.feedback.submit'
860
+ }
861
+ ]
862
+ },
863
+ required: false,
864
+ nullable: true
846
865
  }),
866
+ IsPermissionLogic(),
847
867
  (0,external_class_validator_.IsOptional)(),
848
- _ts_metadata("design:type", Array)
849
- ], CreateFormDto.prototype, "actionGroups", void 0);
868
+ _ts_metadata("design:type", Object)
869
+ ], CreateFormDto.prototype, "permissionLogic", void 0);
850
870
  _ts_decorate([
851
871
  (0,swagger_.ApiProperty)({
852
872
  description: 'Tags used by consuming modules to fetch this form (e.g. survey, review)',
@@ -911,7 +931,7 @@ class FormResponseDto extends dtos_.IdentityResponseDto {
911
931
  schema;
912
932
  schemaVersion;
913
933
  accessType;
914
- actionGroups;
934
+ permissionLogic;
915
935
  tags;
916
936
  companyId;
917
937
  isActive;
@@ -950,9 +970,11 @@ _ts_decorate([
950
970
  ], FormResponseDto.prototype, "accessType", void 0);
951
971
  _ts_decorate([
952
972
  (0,external_class_transformer_.Expose)(),
953
- (0,swagger_.ApiProperty)(),
973
+ (0,swagger_.ApiProperty)({
974
+ nullable: true
975
+ }),
954
976
  _ts_metadata("design:type", Object)
955
- ], FormResponseDto.prototype, "actionGroups", void 0);
977
+ ], FormResponseDto.prototype, "permissionLogic", void 0);
956
978
  _ts_decorate([
957
979
  (0,external_class_transformer_.Expose)(),
958
980
  (0,swagger_.ApiProperty)(),
@@ -981,7 +1003,7 @@ class FormAccessInfoResponseDto extends (0,swagger_.PickType)(FormResponseDto, [
981
1003
  'name',
982
1004
  'description',
983
1005
  'accessType',
984
- 'actionGroups',
1006
+ 'permissionLogic',
985
1007
  'isActive'
986
1008
  ]) {
987
1009
  }
@@ -99,7 +99,7 @@ All queries automatically filter by current user's company context.
99
99
 
100
100
  ### Action Group Forms
101
101
  - Permission-based access via IAM
102
- - User must have specific permissions
102
+ - User must pass the form's \`permissionLogic\` AND / OR rule (an \`ILogicNode\` tree)
103
103
  - Use \`/form-builder/form/authenticated/:id\` to get form
104
104
  - Use \`/form-builder/result/submit\` to submit
105
105
 
@@ -1,4 +1,5 @@
1
1
  import * as __rspack_external__flusys_nestjs_shared_dtos_c4585026 from "@flusys/nestjs-shared/dtos";
2
+ import * as __rspack_external__flusys_nestjs_shared_utils_9125c1f1 from "@flusys/nestjs-shared/utils";
2
3
  import * as __rspack_external__nestjs_swagger_7cd450b5 from "@nestjs/swagger";
3
4
  import * as __rspack_external_class_transformer_c5a479fb from "class-transformer";
4
5
  import * as __rspack_external_class_validator_7b99ef4d from "class-validator";
@@ -270,6 +271,8 @@ __webpack_require__.d(__webpack_exports__, {
270
271
 
271
272
  // EXTERNAL MODULE: external "@flusys/nestjs-shared/dtos"
272
273
  var dtos_ = __webpack_require__(873);
274
+ // EXTERNAL MODULE: external "@flusys/nestjs-shared/utils"
275
+ var utils_ = __webpack_require__(216);
273
276
  // EXTERNAL MODULE: external "@nestjs/swagger"
274
277
  var swagger_ = __webpack_require__(176);
275
278
  // EXTERNAL MODULE: external "class-transformer"
@@ -302,13 +305,23 @@ function _ts_metadata(metadataKey, metadataValue) {
302
305
 
303
306
 
304
307
 
308
+
309
+ function IsPermissionLogic() {
310
+ return (0,external_class_validator_.ValidateBy)({
311
+ name: 'isPermissionLogic',
312
+ validator: {
313
+ validate: (value)=>(0,utils_.permissionLogicProblem)(value) === null,
314
+ defaultMessage: ()=>`$property must be a non-empty AND / OR permission rule with at most ${utils_.MAX_PERMISSION_LOGIC_CODES} permissions`
315
+ }
316
+ });
317
+ }
305
318
  class CreateFormDto {
306
319
  name;
307
320
  description;
308
321
  slug;
309
322
  schema;
310
323
  accessType;
311
- actionGroups;
324
+ permissionLogic;
312
325
  tags;
313
326
  companyId;
314
327
  isActive;
@@ -370,20 +383,28 @@ _ts_decorate([
370
383
  ], CreateFormDto.prototype, "accessType", void 0);
371
384
  _ts_decorate([
372
385
  (0,swagger_.ApiProperty)({
373
- description: 'Action groups for permission-based access (only used when accessType is action_group)',
374
- example: [
375
- 'hr.survey.submit',
376
- 'employee.feedback.submit'
377
- ],
378
- required: false
379
- }),
380
- (0,external_class_validator_.IsArray)(),
381
- (0,external_class_validator_.IsString)({
382
- each: true
383
- }),
386
+ description: 'AND / OR permission rule a user must pass (only used when accessType is action_group)',
387
+ example: {
388
+ type: 'group',
389
+ operator: 'OR',
390
+ children: [
391
+ {
392
+ type: 'action',
393
+ actionId: 'hr.survey.submit'
394
+ },
395
+ {
396
+ type: 'action',
397
+ actionId: 'employee.feedback.submit'
398
+ }
399
+ ]
400
+ },
401
+ required: false,
402
+ nullable: true
403
+ }),
404
+ IsPermissionLogic(),
384
405
  (0,external_class_validator_.IsOptional)(),
385
- _ts_metadata("design:type", Array)
386
- ], CreateFormDto.prototype, "actionGroups", void 0);
406
+ _ts_metadata("design:type", Object)
407
+ ], CreateFormDto.prototype, "permissionLogic", void 0);
387
408
  _ts_decorate([
388
409
  (0,swagger_.ApiProperty)({
389
410
  description: 'Tags used by consuming modules to fetch this form (e.g. survey, review)',
@@ -448,7 +469,7 @@ class FormResponseDto extends dtos_.IdentityResponseDto {
448
469
  schema;
449
470
  schemaVersion;
450
471
  accessType;
451
- actionGroups;
472
+ permissionLogic;
452
473
  tags;
453
474
  companyId;
454
475
  isActive;
@@ -487,9 +508,11 @@ _ts_decorate([
487
508
  ], FormResponseDto.prototype, "accessType", void 0);
488
509
  _ts_decorate([
489
510
  (0,external_class_transformer_.Expose)(),
490
- (0,swagger_.ApiProperty)(),
511
+ (0,swagger_.ApiProperty)({
512
+ nullable: true
513
+ }),
491
514
  _ts_metadata("design:type", Object)
492
- ], FormResponseDto.prototype, "actionGroups", void 0);
515
+ ], FormResponseDto.prototype, "permissionLogic", void 0);
493
516
  _ts_decorate([
494
517
  (0,external_class_transformer_.Expose)(),
495
518
  (0,swagger_.ApiProperty)(),
@@ -518,7 +541,7 @@ class FormAccessInfoResponseDto extends (0,swagger_.PickType)(FormResponseDto, [
518
541
  'name',
519
542
  'description',
520
543
  'accessType',
521
- 'actionGroups',
544
+ 'permissionLogic',
522
545
  'isActive'
523
546
  ]) {
524
547
  }
@@ -691,6 +714,12 @@ __webpack_require__.d(__webpack_exports__, {
691
714
  module.exports = __rspack_external__flusys_nestjs_shared_dtos_c4585026;
692
715
 
693
716
 
717
+ },
718
+ 216(module) {
719
+
720
+ module.exports = __rspack_external__flusys_nestjs_shared_utils_9125c1f1;
721
+
722
+
694
723
  },
695
724
  176(module) {
696
725
 
@@ -56,7 +56,7 @@ class Form extends entities_Identity {
56
56
  schema;
57
57
  schemaVersion;
58
58
  accessType;
59
- actionGroups;
59
+ permissionLogic;
60
60
  tags;
61
61
  isActive;
62
62
  }
@@ -113,10 +113,10 @@ _ts_decorate([
113
113
  (0,external_typeorm_.Column)({
114
114
  type: getJsonColumnType(),
115
115
  nullable: true,
116
- name: 'action_groups'
116
+ name: 'permission_logic'
117
117
  }),
118
118
  _ts_metadata("design:type", Object)
119
- ], Form.prototype, "actionGroups", void 0);
119
+ ], Form.prototype, "permissionLogic", void 0);
120
120
  _ts_decorate([
121
121
  (0,external_typeorm_.Column)({
122
122
  type: getJsonColumnType(),
package/fesm/index.js CHANGED
@@ -118,7 +118,7 @@ All queries automatically filter by current user's company context.
118
118
 
119
119
  ### Action Group Forms
120
120
  - Permission-based access via IAM
121
- - User must have specific permissions
121
+ - User must pass the form's \`permissionLogic\` AND / OR rule (an \`ILogicNode\` tree)
122
122
  - Use \`/form-builder/form/authenticated/:id\` to get form
123
123
  - Use \`/form-builder/result/submit\` to submit
124
124
 
@@ -239,7 +239,7 @@ __webpack_require__.d(__webpack_exports__, {
239
239
  XZ: () => (/* reexport */ computed_field_utils/* .calculateComputedFields */.X),
240
240
  aY: () => (/* reexport */ docs/* .formBuilderSwaggerConfig */.a),
241
241
  iO: () => (/* reexport */ entities.getFormBuilderEntitiesByConfig),
242
- io: () => (/* reexport */ permission_utils/* .validateUserPermissions */.i)
242
+ EY: () => (/* reexport */ permission_utils/* .hasFormPermission */.E)
243
243
  });
244
244
 
245
245
  // EXTERNAL MODULE: ./projects/nestjs-form-builder/src/config/index.ts
@@ -533,5 +533,5 @@ var __webpack_exports__VerifyOtpResponseDto = __webpack_exports__.TU;
533
533
  var __webpack_exports__calculateComputedFields = __webpack_exports__.XZ;
534
534
  var __webpack_exports__formBuilderSwaggerConfig = __webpack_exports__.aY;
535
535
  var __webpack_exports__getFormBuilderEntitiesByConfig = __webpack_exports__.iO;
536
- var __webpack_exports__validateUserPermissions = __webpack_exports__.io;
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__validateUserPermissions as validateUserPermissions };
536
+ 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 };
@@ -1,4 +1,4 @@
1
- import { type IIdentity } from '@flusys/nestjs-shared/interfaces';
1
+ import { type IIdentity, type ILogicNode } from '@flusys/nestjs-shared/interfaces';
2
2
  import { FormAccessType } from '../enums/form-access-type.enum.js';
3
3
  export interface IForm extends IIdentity {
4
4
  name: string;
@@ -7,7 +7,7 @@ export interface IForm extends IIdentity {
7
7
  schema: Record<string, unknown>;
8
8
  schemaVersion: number;
9
9
  accessType: FormAccessType;
10
- actionGroups: string[] | null;
10
+ permissionLogic: ILogicNode | null;
11
11
  tags: string[] | null;
12
12
  isActive: boolean;
13
13
  companyId: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/nestjs-form-builder",
3
- "version": "9.1.0-rc.1",
3
+ "version": "9.1.1",
4
4
  "description": "Dynamic form builder module with schema versioning and access control",
5
5
  "type": "module",
6
6
  "main": "./fesm/index.js",
@@ -65,17 +65,17 @@
65
65
  "@nestjs/common": "^12.0.0",
66
66
  "@nestjs/config": "^12.0.0",
67
67
  "@nestjs/core": "^12.0.0",
68
- "@nestjs/platform-express": "^12.0.0",
68
+ "@nestjs/platform-express": "^12.1.2",
69
69
  "@nestjs/swagger": "^12.0.0",
70
70
  "@nestjs/throttler": "^6.5.0",
71
71
  "@nestjs/typeorm": "^12.0.0",
72
72
  "class-transformer": "^0.5.1",
73
73
  "class-validator": "^0.15.0",
74
- "typeorm": "^0.3.0",
74
+ "typeorm": "^0.3.31",
75
75
  "express": "^5.0.0"
76
76
  },
77
77
  "dependencies": {
78
- "@flusys/nestjs-core": "9.1.0-rc.1",
79
- "@flusys/nestjs-shared": "9.1.0-rc.1"
78
+ "@flusys/nestjs-core": "9.1.1",
79
+ "@flusys/nestjs-shared": "9.1.1"
80
80
  }
81
81
  }
@@ -1,7 +1,7 @@
1
1
  import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
2
2
  import { FilterAndPaginationDto } from '@flusys/nestjs-shared/dtos';
3
- import { type ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
4
- import { UtilsService } from '@flusys/nestjs-shared/modules';
3
+ import { type ILoggedUserInfo, type IPermissionResolver } from '@flusys/nestjs-shared/interfaces';
4
+ import { SharedPermissionCacheService, UtilsService } from '@flusys/nestjs-shared/modules';
5
5
  import { SelectQueryBuilder } from 'typeorm';
6
6
  import { CreateFormResultDto, SubmitFormDto, UpdateFormResultDto } from '../dtos/form-result.dto.js';
7
7
  import { FormResult } from '../entities/index.js';
@@ -15,9 +15,11 @@ export declare class FormResultService extends ApiService<CreateFormResultDto, U
15
15
  private readonly formBuilderConfig;
16
16
  private readonly dataSourceProvider;
17
17
  private readonly formEmailAuthService;
18
+ private readonly permissionResolver?;
19
+ private readonly permissionCache?;
18
20
  private formRepository;
19
21
  private static readonly submissionLocks;
20
- constructor(cacheManager: HybridCache, utilsService: UtilsService, formBuilderConfig: FormBuilderConfigService, dataSourceProvider: FormBuilderDataSourceProvider, formEmailAuthService: FormEmailAuthService);
22
+ constructor(cacheManager: HybridCache, utilsService: UtilsService, formBuilderConfig: FormBuilderConfigService, dataSourceProvider: FormBuilderDataSourceProvider, formEmailAuthService: FormEmailAuthService, permissionResolver?: IPermissionResolver | undefined, permissionCache?: SharedPermissionCacheService | undefined);
21
23
  private ensureFormRepositoryInitialized;
22
24
  private withSubmissionLock;
23
25
  private getActiveForm;
@@ -1,7 +1,7 @@
1
1
  import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
2
2
  import { FilterAndPaginationDto } from '@flusys/nestjs-shared/dtos';
3
- import { type ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
4
- import { UtilsService } from '@flusys/nestjs-shared/modules';
3
+ import { type ILoggedUserInfo, type ILogicNode, type IPermissionResolver } from '@flusys/nestjs-shared/interfaces';
4
+ import { SharedPermissionCacheService, UtilsService } from '@flusys/nestjs-shared/modules';
5
5
  import { SelectQueryBuilder } from 'typeorm';
6
6
  import { CreateFormDto, UpdateFormDto } from '../dtos/form.dto.js';
7
7
  import { Form } from '../entities/index.js';
@@ -13,7 +13,9 @@ export declare class FormService extends ApiService<CreateFormDto, UpdateFormDto
13
13
  protected cacheManager: HybridCache;
14
14
  protected utilsService: UtilsService;
15
15
  private readonly formBuilderConfig;
16
- constructor(cacheManager: HybridCache, utilsService: UtilsService, formBuilderConfig: FormBuilderConfigService, dataSourceProvider: FormBuilderDataSourceProvider);
16
+ private readonly permissionResolver?;
17
+ private readonly permissionCache?;
18
+ constructor(cacheManager: HybridCache, utilsService: UtilsService, formBuilderConfig: FormBuilderConfigService, dataSourceProvider: FormBuilderDataSourceProvider, permissionResolver?: IPermissionResolver | undefined, permissionCache?: SharedPermissionCacheService | undefined);
17
19
  convertSingleDtoToEntity(dto: CreateFormDto | UpdateFormDto, user: ILoggedUserInfo | null): Promise<Form>;
18
20
  getSelectQuery(query: SelectQueryBuilder<Form>, _user: ILoggedUserInfo | null, select?: string[]): Promise<{
19
21
  query: SelectQueryBuilder<Form>;
@@ -40,7 +42,7 @@ export declare class FormService extends ApiService<CreateFormDto, UpdateFormDto
40
42
  name: string;
41
43
  description: string | null;
42
44
  accessType: FormAccessType;
43
- actionGroups: string[] | null;
45
+ permissionLogic: ILogicNode | null;
44
46
  isActive: boolean;
45
47
  }>;
46
48
  getEmailVerifiedFormEntity(formId: string): Promise<Form>;
@@ -1,3 +1,3 @@
1
- import { HybridCache } from '@flusys/nestjs-shared/classes';
2
- import { type ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
3
- export declare function validateUserPermissions(user: ILoggedUserInfo, requiredPermissions: string[], cacheManager: HybridCache, enableCompanyFeature: boolean): Promise<boolean>;
1
+ import { type ILoggedUserInfo, type ILogicNode } from '@flusys/nestjs-shared/interfaces';
2
+ import { type IPermissionSources } from '@flusys/nestjs-shared/utils';
3
+ export declare function hasFormPermission(user: ILoggedUserInfo, logic: ILogicNode, sources: IPermissionSources): Promise<boolean>;