@flusys/nestjs-iam 7.0.2 → 8.0.0-beta.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/config/index.d.ts +3 -3
- package/controllers/action.controller.d.ts +4 -4
- package/controllers/company-action-permission.controller.d.ts +2 -2
- package/controllers/index.d.ts +6 -6
- package/controllers/my-permission.controller.d.ts +3 -3
- package/controllers/role-permission.controller.d.ts +4 -4
- package/controllers/role.controller.d.ts +2 -2
- package/controllers/user-action-permission.controller.d.ts +4 -4
- package/docs/iam-swagger.config.d.ts +2 -2
- package/docs/index.d.ts +1 -1
- package/dtos/action.dto.d.ts +2 -2
- package/dtos/index.d.ts +3 -3
- package/entities/action.entity.d.ts +2 -2
- package/entities/index.d.ts +11 -11
- package/entities/permission-with-company.entity.d.ts +1 -1
- package/entities/role-with-company.entity.d.ts +1 -1
- package/entities/role.entity.d.ts +1 -1
- package/entities/user-iam-permission.entity.d.ts +1 -1
- package/enums/index.d.ts +4 -4
- package/fesm/284.js +1262 -0
- package/fesm/867.js +3699 -0
- package/fesm/chunks/364.js +86 -0
- package/fesm/config/index.js +133 -3
- package/fesm/controllers/index.js +18 -7
- package/fesm/docs/index.js +240 -1
- package/fesm/dtos/index.js +1090 -3
- package/fesm/entities/index.js +670 -31
- package/fesm/enums/index.js +97 -4
- package/fesm/helpers/index.js +125 -2
- package/fesm/index.js +806 -11
- package/fesm/interfaces/index.js +17 -3
- package/fesm/modules/index.js +259 -1
- package/fesm/runtime.js +134 -0
- package/fesm/services/index.js +43 -6
- package/fesm/types/index.js +15 -1
- package/helpers/company-access.helper.d.ts +2 -2
- package/helpers/index.d.ts +2 -2
- package/helpers/permission-mode.helper.d.ts +1 -1
- package/index.d.ts +11 -11
- package/interfaces/action.interface.d.ts +2 -2
- package/interfaces/iam-module-options.interface.d.ts +3 -3
- package/interfaces/index.d.ts +3 -3
- package/modules/iam.module.d.ts +2 -2
- package/modules/index.d.ts +1 -1
- package/package.json +30 -37
- package/services/action.service.d.ts +9 -9
- package/services/iam-config.service.d.ts +4 -4
- package/services/iam-datasource.service.d.ts +3 -3
- package/services/index.d.ts +6 -6
- package/services/permission-cache.service.d.ts +5 -5
- package/services/permission.service.d.ts +4 -4
- package/services/role.service.d.ts +8 -8
- package/types/index.d.ts +1 -1
- package/cjs/config/event-actions.js +0 -36
- package/cjs/config/iam.constants.js +0 -11
- package/cjs/config/index.js +0 -20
- package/cjs/config/message-keys.js +0 -71
- package/cjs/controllers/action.controller.js +0 -174
- package/cjs/controllers/action.controller.spec.js +0 -79
- package/cjs/controllers/company-action-permission.controller.js +0 -119
- package/cjs/controllers/company-action-permission.controller.spec.js +0 -65
- package/cjs/controllers/index.js +0 -23
- package/cjs/controllers/my-permission.controller.js +0 -147
- package/cjs/controllers/my-permission.controller.spec.js +0 -128
- package/cjs/controllers/role-permission.controller.js +0 -191
- package/cjs/controllers/role-permission.controller.spec.js +0 -140
- package/cjs/controllers/role.controller.js +0 -102
- package/cjs/controllers/role.controller.spec.js +0 -86
- package/cjs/controllers/user-action-permission.controller.js +0 -129
- package/cjs/controllers/user-action-permission.controller.spec.js +0 -68
- package/cjs/docs/iam-swagger.config.js +0 -209
- package/cjs/docs/iam-swagger.config.spec.js +0 -99
- package/cjs/docs/index.js +0 -18
- package/cjs/dtos/action.dto.js +0 -282
- package/cjs/dtos/index.js +0 -21
- package/cjs/dtos/permission.dto.js +0 -637
- package/cjs/dtos/role.dto.js +0 -155
- package/cjs/entities/action.entity.js +0 -138
- package/cjs/entities/index.js +0 -80
- package/cjs/entities/index.spec.js +0 -56
- package/cjs/entities/permission-base.entity.js +0 -107
- package/cjs/entities/permission-with-company.entity.js +0 -100
- package/cjs/entities/role-base.entity.js +0 -80
- package/cjs/entities/role-with-company.entity.js +0 -55
- package/cjs/entities/role.entity.js +0 -25
- package/cjs/entities/user-iam-permission.entity.js +0 -43
- package/cjs/enums/action-type.enum.js +0 -22
- package/cjs/enums/iam-entity-type.enum.js +0 -17
- package/cjs/enums/iam-permission-type.enum.js +0 -17
- package/cjs/enums/index.js +0 -21
- package/cjs/enums/permission-type.enum.js +0 -16
- package/cjs/helpers/company-access.helper.js +0 -23
- package/cjs/helpers/company-access.helper.spec.js +0 -68
- package/cjs/helpers/index.js +0 -19
- package/cjs/helpers/permission-mode.helper.js +0 -49
- package/cjs/helpers/permission-mode.helper.spec.js +0 -54
- package/cjs/index.js +0 -28
- package/cjs/interfaces/action.interface.js +0 -4
- package/cjs/interfaces/iam-module-options.interface.js +0 -4
- package/cjs/interfaces/index.js +0 -20
- package/cjs/interfaces/role.interface.js +0 -7
- package/cjs/modules/iam.module.js +0 -186
- package/cjs/modules/iam.module.spec.js +0 -282
- package/cjs/modules/index.js +0 -18
- package/cjs/services/action.service.js +0 -337
- package/cjs/services/action.service.spec.js +0 -351
- package/cjs/services/iam-config.service.js +0 -87
- package/cjs/services/iam-config.service.spec.js +0 -110
- package/cjs/services/iam-datasource.service.js +0 -129
- package/cjs/services/iam-datasource.service.spec.js +0 -110
- package/cjs/services/index.js +0 -23
- package/cjs/services/permission-cache.service.js +0 -544
- package/cjs/services/permission-cache.service.spec.js +0 -718
- package/cjs/services/permission.service.js +0 -914
- package/cjs/services/permission.service.spec.js +0 -918
- package/cjs/services/role.service.js +0 -224
- package/cjs/services/role.service.spec.js +0 -347
- package/cjs/types/index.js +0 -18
- package/cjs/types/logic-node.type.js +0 -30
- package/fesm/config/event-actions.js +0 -15
- package/fesm/config/iam.constants.js +0 -1
- package/fesm/config/message-keys.js +0 -35
- package/fesm/controllers/action.controller.js +0 -164
- package/fesm/controllers/action.controller.spec.js +0 -75
- package/fesm/controllers/company-action-permission.controller.js +0 -109
- package/fesm/controllers/company-action-permission.controller.spec.js +0 -61
- package/fesm/controllers/my-permission.controller.js +0 -137
- package/fesm/controllers/my-permission.controller.spec.js +0 -124
- package/fesm/controllers/role-permission.controller.js +0 -181
- package/fesm/controllers/role-permission.controller.spec.js +0 -136
- package/fesm/controllers/role.controller.js +0 -92
- package/fesm/controllers/role.controller.spec.js +0 -82
- package/fesm/controllers/user-action-permission.controller.js +0 -119
- package/fesm/controllers/user-action-permission.controller.spec.js +0 -64
- package/fesm/docs/iam-swagger.config.js +0 -199
- package/fesm/docs/iam-swagger.config.spec.js +0 -95
- package/fesm/dtos/action.dto.js +0 -252
- package/fesm/dtos/permission.dto.js +0 -571
- package/fesm/dtos/role.dto.js +0 -134
- package/fesm/entities/action.entity.js +0 -128
- package/fesm/entities/index.spec.js +0 -52
- package/fesm/entities/permission-base.entity.js +0 -100
- package/fesm/entities/permission-with-company.entity.js +0 -90
- package/fesm/entities/role-base.entity.js +0 -70
- package/fesm/entities/role-with-company.entity.js +0 -45
- package/fesm/entities/role.entity.js +0 -15
- package/fesm/entities/user-iam-permission.entity.js +0 -33
- package/fesm/enums/action-type.enum.js +0 -12
- package/fesm/enums/iam-entity-type.enum.js +0 -7
- package/fesm/enums/iam-permission-type.enum.js +0 -7
- package/fesm/enums/permission-type.enum.js +0 -6
- package/fesm/helpers/company-access.helper.js +0 -18
- package/fesm/helpers/company-access.helper.spec.js +0 -64
- package/fesm/helpers/permission-mode.helper.js +0 -49
- package/fesm/helpers/permission-mode.helper.spec.js +0 -50
- package/fesm/interfaces/action.interface.js +0 -3
- package/fesm/interfaces/iam-module-options.interface.js +0 -3
- package/fesm/interfaces/role.interface.js +0 -4
- package/fesm/modules/iam.module.js +0 -176
- package/fesm/modules/iam.module.spec.js +0 -278
- package/fesm/services/action.service.js +0 -327
- package/fesm/services/action.service.spec.js +0 -347
- package/fesm/services/iam-config.service.js +0 -77
- package/fesm/services/iam-config.service.spec.js +0 -106
- package/fesm/services/iam-datasource.service.js +0 -78
- package/fesm/services/iam-datasource.service.spec.js +0 -106
- package/fesm/services/permission-cache.service.js +0 -527
- package/fesm/services/permission-cache.service.spec.js +0 -714
- package/fesm/services/permission.service.js +0 -904
- package/fesm/services/permission.service.spec.js +0 -914
- package/fesm/services/role.service.js +0 -214
- package/fesm/services/role.service.spec.js +0 -343
- package/fesm/types/logic-node.type.js +0 -27
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
function _define_property(obj, key, value) {
|
|
2
|
-
if (key in obj) {
|
|
3
|
-
Object.defineProperty(obj, key, {
|
|
4
|
-
value: value,
|
|
5
|
-
enumerable: true,
|
|
6
|
-
configurable: true,
|
|
7
|
-
writable: true
|
|
8
|
-
});
|
|
9
|
-
} else {
|
|
10
|
-
obj[key] = value;
|
|
11
|
-
}
|
|
12
|
-
return obj;
|
|
13
|
-
}
|
|
14
|
-
function _ts_decorate(decorators, target, key, desc) {
|
|
15
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
17
|
-
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
18
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
|
-
}
|
|
20
|
-
function _ts_metadata(k, v) {
|
|
21
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
|
-
}
|
|
23
|
-
function _ts_param(paramIndex, decorator) {
|
|
24
|
-
return function(target, key) {
|
|
25
|
-
decorator(target, key, paramIndex);
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
import { ApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
|
-
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
-
import { BadRequestException, ForbiddenException, Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
|
-
import { In, IsNull } from 'typeorm';
|
|
32
|
-
import { ACTION_MESSAGES, IAM_EVENT_ENTITIES, IAM_EVENT_MODULE, PERMISSION_OPERATION_MESSAGES } from '../config';
|
|
33
|
-
import { Action } from '../entities/action.entity';
|
|
34
|
-
import { UserIamPermissionWithCompany } from '../entities/permission-with-company.entity';
|
|
35
|
-
import { UserIamPermission } from '../entities/user-iam-permission.entity';
|
|
36
|
-
import { IamEntityType } from '../enums/iam-entity-type.enum';
|
|
37
|
-
import { IamPermissionType } from '../enums/iam-permission-type.enum';
|
|
38
|
-
import { IAMConfigService } from './iam-config.service';
|
|
39
|
-
import { IAMDataSourceService } from './iam-datasource.service';
|
|
40
|
-
import { PermissionCacheService } from './permission-cache.service';
|
|
41
|
-
import { PermissionService } from './permission.service';
|
|
42
|
-
export class ActionService extends ApiService {
|
|
43
|
-
async beforeUpdateOperation(dto, _user, _queryRunner) {
|
|
44
|
-
const dtos = Array.isArray(dto) ? dto : [
|
|
45
|
-
dto
|
|
46
|
-
];
|
|
47
|
-
const ids = dtos.map((d)=>d.id);
|
|
48
|
-
await this.ensureDataSourceRepository();
|
|
49
|
-
const readonlyActions = await this.repository.find({
|
|
50
|
-
where: {
|
|
51
|
-
id: In(ids),
|
|
52
|
-
readOnly: true,
|
|
53
|
-
deletedAt: IsNull()
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
if (readonlyActions.length > 0) {
|
|
57
|
-
const names = readonlyActions.map((a)=>a.name).join(', ');
|
|
58
|
-
throw new ForbiddenException({
|
|
59
|
-
message: `Cannot modify readonly action(s): ${names}`,
|
|
60
|
-
messageKey: ACTION_MESSAGES.READONLY_UPDATE_FORBIDDEN,
|
|
61
|
-
messageVariables: {
|
|
62
|
-
names
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
async beforeDeleteOperation(dto, user, queryRunner) {
|
|
68
|
-
const actionIds = Array.isArray(dto.id) ? dto.id : [
|
|
69
|
-
dto.id
|
|
70
|
-
];
|
|
71
|
-
if (actionIds.length === 0) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
await this.ensureDataSourceRepository();
|
|
75
|
-
const readonlyActions = await this.repository.find({
|
|
76
|
-
where: {
|
|
77
|
-
id: In(actionIds),
|
|
78
|
-
readOnly: true,
|
|
79
|
-
deletedAt: IsNull()
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
if (readonlyActions.length > 0) {
|
|
83
|
-
const names = readonlyActions.map((a)=>a.name).join(', ');
|
|
84
|
-
throw new ForbiddenException({
|
|
85
|
-
message: `Cannot delete readonly action(s): ${names}`,
|
|
86
|
-
messageKey: ACTION_MESSAGES.READONLY_DELETE_FORBIDDEN,
|
|
87
|
-
messageVariables: {
|
|
88
|
-
names
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
93
|
-
const permissionEntity = enableCompanyFeature ? UserIamPermissionWithCompany : UserIamPermission;
|
|
94
|
-
const permissionRepo = queryRunner.manager.getRepository(permissionEntity);
|
|
95
|
-
const [roleActionRows, userActionRows, companyActionRows] = await Promise.all([
|
|
96
|
-
permissionRepo.find({
|
|
97
|
-
where: {
|
|
98
|
-
permissionType: IamPermissionType.ROLE_ACTION,
|
|
99
|
-
sourceType: IamEntityType.ROLE,
|
|
100
|
-
targetType: IamEntityType.ACTION,
|
|
101
|
-
targetId: In(actionIds)
|
|
102
|
-
}
|
|
103
|
-
}),
|
|
104
|
-
permissionRepo.find({
|
|
105
|
-
where: {
|
|
106
|
-
permissionType: IamPermissionType.USER_ACTION,
|
|
107
|
-
targetType: IamEntityType.ACTION,
|
|
108
|
-
targetId: In(actionIds)
|
|
109
|
-
}
|
|
110
|
-
}),
|
|
111
|
-
enableCompanyFeature ? permissionRepo.find({
|
|
112
|
-
where: {
|
|
113
|
-
permissionType: IamPermissionType.COMPANY_ACTION,
|
|
114
|
-
sourceType: IamEntityType.COMPANY,
|
|
115
|
-
targetType: IamEntityType.ACTION,
|
|
116
|
-
targetId: In(actionIds)
|
|
117
|
-
}
|
|
118
|
-
}) : Promise.resolve([])
|
|
119
|
-
]);
|
|
120
|
-
const affectedUserIds = new Set(userActionRows.map((row)=>row.sourceId));
|
|
121
|
-
const affectedCompanyIds = [
|
|
122
|
-
...new Set(companyActionRows.map((row)=>row.sourceId).filter((id)=>!!id))
|
|
123
|
-
];
|
|
124
|
-
const affectedRoleIds = [
|
|
125
|
-
...new Set(roleActionRows.map((row)=>row.sourceId))
|
|
126
|
-
];
|
|
127
|
-
if (affectedRoleIds.length > 0) {
|
|
128
|
-
const userRoleRows = await permissionRepo.find({
|
|
129
|
-
where: {
|
|
130
|
-
permissionType: IamPermissionType.USER_ROLE,
|
|
131
|
-
targetType: IamEntityType.ROLE,
|
|
132
|
-
targetId: In(affectedRoleIds)
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
userRoleRows.forEach((row)=>affectedUserIds.add(row.sourceId));
|
|
136
|
-
}
|
|
137
|
-
await permissionRepo.delete({
|
|
138
|
-
permissionType: IamPermissionType.ROLE_ACTION,
|
|
139
|
-
targetType: IamEntityType.ACTION,
|
|
140
|
-
targetId: In(actionIds)
|
|
141
|
-
});
|
|
142
|
-
await permissionRepo.delete({
|
|
143
|
-
permissionType: IamPermissionType.USER_ACTION,
|
|
144
|
-
targetType: IamEntityType.ACTION,
|
|
145
|
-
targetId: In(actionIds)
|
|
146
|
-
});
|
|
147
|
-
await permissionRepo.delete({
|
|
148
|
-
permissionType: IamPermissionType.COMPANY_ACTION,
|
|
149
|
-
sourceType: IamEntityType.COMPANY,
|
|
150
|
-
targetType: IamEntityType.ACTION,
|
|
151
|
-
targetId: In(actionIds)
|
|
152
|
-
});
|
|
153
|
-
await Promise.all([
|
|
154
|
-
affectedUserIds.size > 0 ? this.permissionCacheService.invalidateUsers([
|
|
155
|
-
...affectedUserIds
|
|
156
|
-
]) : Promise.resolve(),
|
|
157
|
-
...affectedCompanyIds.map((companyId)=>this.permissionCacheService.invalidateCompanyMembersCache(companyId))
|
|
158
|
-
]);
|
|
159
|
-
}
|
|
160
|
-
// Query Customization
|
|
161
|
-
async getSelectQuery(query, _user, select) {
|
|
162
|
-
if (!select?.length) {
|
|
163
|
-
select = [
|
|
164
|
-
'id',
|
|
165
|
-
'name',
|
|
166
|
-
'code',
|
|
167
|
-
'description',
|
|
168
|
-
'actionType',
|
|
169
|
-
'permissionLogic',
|
|
170
|
-
'isActive',
|
|
171
|
-
'readOnly',
|
|
172
|
-
'parentId',
|
|
173
|
-
'serial',
|
|
174
|
-
'createdAt'
|
|
175
|
-
];
|
|
176
|
-
}
|
|
177
|
-
query.select(select.map((f)=>`${this.entityName}.${f}`));
|
|
178
|
-
return {
|
|
179
|
-
query,
|
|
180
|
-
isRaw: false
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
async getGlobalSearchQuery(query, search) {
|
|
184
|
-
query.andWhere('(action.name LIKE :search OR action.code LIKE :search OR action.description LIKE :search)', {
|
|
185
|
-
search: `%${search}%`
|
|
186
|
-
});
|
|
187
|
-
return {
|
|
188
|
-
query,
|
|
189
|
-
isRaw: false
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
// Response Conversion
|
|
193
|
-
convertEntityToResponseDto(entity, _isRaw) {
|
|
194
|
-
return {
|
|
195
|
-
id: entity.id,
|
|
196
|
-
readOnly: entity.readOnly,
|
|
197
|
-
name: entity.name,
|
|
198
|
-
description: entity.description,
|
|
199
|
-
code: entity.code,
|
|
200
|
-
actionType: entity.actionType,
|
|
201
|
-
permissionLogic: entity.permissionLogic,
|
|
202
|
-
serial: entity.serial,
|
|
203
|
-
isActive: entity.isActive,
|
|
204
|
-
parentId: entity.parentId,
|
|
205
|
-
createdAt: entity.createdAt,
|
|
206
|
-
updatedAt: entity.updatedAt,
|
|
207
|
-
deletedAt: entity.deletedAt,
|
|
208
|
-
createdById: entity.createdById,
|
|
209
|
-
updatedById: entity.updatedById,
|
|
210
|
-
deletedById: entity.deletedById
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
requireUser(user, methodName) {
|
|
214
|
-
if (!user) {
|
|
215
|
-
throw new BadRequestException({
|
|
216
|
-
message: `User is required for ${methodName}`,
|
|
217
|
-
messageKey: PERMISSION_OPERATION_MESSAGES.USER_REQUIRED,
|
|
218
|
-
messageVariables: {
|
|
219
|
-
method: methodName
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
/** Get actions available for permission assignment (filtered by company whitelist) */ async getActionsForPermission(user, companyId) {
|
|
225
|
-
await this.ensureDataSourceRepository();
|
|
226
|
-
this.requireUser(user, 'getActionsForPermission');
|
|
227
|
-
let whereClause = {};
|
|
228
|
-
const targetCompanyId = companyId || user.companyId;
|
|
229
|
-
if (this.iamConfigService.isCompanyFeatureEnabled() && targetCompanyId) {
|
|
230
|
-
const companyActionIds = await this.permissionService.getCompanyActions(targetCompanyId, true);
|
|
231
|
-
if (companyActionIds.length === 0) {
|
|
232
|
-
return [];
|
|
233
|
-
}
|
|
234
|
-
whereClause = {
|
|
235
|
-
id: In(companyActionIds)
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
const actions = await this.repository.find({
|
|
239
|
-
where: whereClause,
|
|
240
|
-
select: this.actionSelectFields
|
|
241
|
-
});
|
|
242
|
-
return actions.map((action)=>this.convertEntityToResponseDto(action, false));
|
|
243
|
-
}
|
|
244
|
-
/** Get actions in hierarchical tree structure */ async getActionTree(user, search, isActive, withDeleted = false) {
|
|
245
|
-
await this.ensureDataSourceRepository();
|
|
246
|
-
this.requireUser(user, 'getActionTree');
|
|
247
|
-
const query = this.repository.createQueryBuilder('action');
|
|
248
|
-
if (!withDeleted) {
|
|
249
|
-
query.andWhere('action.deletedAt IS NULL');
|
|
250
|
-
}
|
|
251
|
-
if (isActive !== undefined) {
|
|
252
|
-
query.andWhere('action.isActive = :isActive', {
|
|
253
|
-
isActive
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
if (search?.trim()) {
|
|
257
|
-
query.andWhere('(action.name LIKE :search OR action.code LIKE :search)', {
|
|
258
|
-
search: `%${search.trim()}%`
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
const actions = await query.orderBy('action.serial', 'ASC').getMany();
|
|
262
|
-
return this.buildActionTree(actions);
|
|
263
|
-
}
|
|
264
|
-
buildActionTree(actions) {
|
|
265
|
-
if (!actions?.length) {
|
|
266
|
-
return [];
|
|
267
|
-
}
|
|
268
|
-
const map = new Map();
|
|
269
|
-
const rootNodes = [];
|
|
270
|
-
for (const action of actions){
|
|
271
|
-
const treeNode = {
|
|
272
|
-
...this.convertEntityToResponseDto(action, false),
|
|
273
|
-
children: []
|
|
274
|
-
};
|
|
275
|
-
map.set(action.id, treeNode);
|
|
276
|
-
}
|
|
277
|
-
for (const action of actions){
|
|
278
|
-
const node = map.get(action.id);
|
|
279
|
-
if (!node) {
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (action.parentId && map.has(action.parentId)) {
|
|
283
|
-
const parent = map.get(action.parentId);
|
|
284
|
-
if (parent?.children) {
|
|
285
|
-
parent.children.push(node);
|
|
286
|
-
}
|
|
287
|
-
} else {
|
|
288
|
-
rootNodes.push(node);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return rootNodes;
|
|
292
|
-
}
|
|
293
|
-
constructor(cacheManager, utilsService, iamConfigService, dataSourceProvider, permissionService, permissionCacheService){
|
|
294
|
-
super(IAM_EVENT_ENTITIES.ACTION, cacheManager, utilsService, ActionService.name, true, IAM_EVENT_MODULE, dataSourceProvider, Action), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "iamConfigService", void 0), _define_property(this, "permissionService", void 0), _define_property(this, "permissionCacheService", void 0), // Custom Methods
|
|
295
|
-
_define_property(this, "actionSelectFields", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.iamConfigService = iamConfigService, this.permissionService = permissionService, this.permissionCacheService = permissionCacheService, this.actionSelectFields = [
|
|
296
|
-
'id',
|
|
297
|
-
'code',
|
|
298
|
-
'name',
|
|
299
|
-
'description',
|
|
300
|
-
'actionType',
|
|
301
|
-
'permissionLogic',
|
|
302
|
-
'isActive',
|
|
303
|
-
'parentId',
|
|
304
|
-
'serial'
|
|
305
|
-
];
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
ActionService = _ts_decorate([
|
|
309
|
-
Injectable({
|
|
310
|
-
scope: Scope.REQUEST
|
|
311
|
-
}),
|
|
312
|
-
_ts_param(0, Inject('CACHE_INSTANCE')),
|
|
313
|
-
_ts_param(1, Inject(UtilsService)),
|
|
314
|
-
_ts_param(2, Inject(IAMConfigService)),
|
|
315
|
-
_ts_param(3, Inject(IAMDataSourceService)),
|
|
316
|
-
_ts_param(4, Inject(PermissionService)),
|
|
317
|
-
_ts_param(5, Inject(PermissionCacheService)),
|
|
318
|
-
_ts_metadata("design:type", Function),
|
|
319
|
-
_ts_metadata("design:paramtypes", [
|
|
320
|
-
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
321
|
-
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
322
|
-
typeof IAMConfigService === "undefined" ? Object : IAMConfigService,
|
|
323
|
-
typeof IAMDataSourceService === "undefined" ? Object : IAMDataSourceService,
|
|
324
|
-
typeof PermissionService === "undefined" ? Object : PermissionService,
|
|
325
|
-
typeof PermissionCacheService === "undefined" ? Object : PermissionCacheService
|
|
326
|
-
])
|
|
327
|
-
], ActionService);
|
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
import { Test } from '@nestjs/testing';
|
|
2
|
-
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
3
|
-
import { createMockDataSourceProvider, createMockRepository } from '@test-utils/mocks/repository.mock';
|
|
4
|
-
import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
|
|
5
|
-
import { ActionType } from '../enums';
|
|
6
|
-
import { ActionService } from './action.service';
|
|
7
|
-
import { IAMConfigService } from './iam-config.service';
|
|
8
|
-
import { IAMDataSourceService } from './iam-datasource.service';
|
|
9
|
-
import { PermissionCacheService } from './permission-cache.service';
|
|
10
|
-
import { PermissionService } from './permission.service';
|
|
11
|
-
function buildAction(overrides = {}) {
|
|
12
|
-
return {
|
|
13
|
-
id: 'action-uuid-1',
|
|
14
|
-
readOnly: false,
|
|
15
|
-
name: 'View Products',
|
|
16
|
-
description: null,
|
|
17
|
-
code: 'products.read',
|
|
18
|
-
actionType: ActionType.BACKEND,
|
|
19
|
-
permissionLogic: null,
|
|
20
|
-
serial: 1,
|
|
21
|
-
isActive: true,
|
|
22
|
-
parentId: null,
|
|
23
|
-
createdAt: new Date(),
|
|
24
|
-
updatedAt: new Date(),
|
|
25
|
-
deletedAt: null,
|
|
26
|
-
createdById: null,
|
|
27
|
-
updatedById: null,
|
|
28
|
-
deletedById: null,
|
|
29
|
-
...overrides
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
describe('ActionService', ()=>{
|
|
33
|
-
let service;
|
|
34
|
-
let mockRepo;
|
|
35
|
-
let mockIamConfigService;
|
|
36
|
-
let mockPermissionService;
|
|
37
|
-
let mockPermissionCacheService;
|
|
38
|
-
beforeEach(async ()=>{
|
|
39
|
-
mockRepo = createMockRepository();
|
|
40
|
-
mockRepo.find.mockResolvedValue([]);
|
|
41
|
-
const mockDataSourceProvider = createMockDataSourceProvider(mockRepo);
|
|
42
|
-
mockIamConfigService = {
|
|
43
|
-
isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
|
|
44
|
-
};
|
|
45
|
-
mockPermissionService = {
|
|
46
|
-
getCompanyActions: jest.fn()
|
|
47
|
-
};
|
|
48
|
-
mockPermissionCacheService = {
|
|
49
|
-
invalidateUsers: jest.fn(),
|
|
50
|
-
invalidateCompanyMembersCache: jest.fn()
|
|
51
|
-
};
|
|
52
|
-
const module = await Test.createTestingModule({
|
|
53
|
-
providers: [
|
|
54
|
-
ActionService,
|
|
55
|
-
{
|
|
56
|
-
provide: 'CACHE_INSTANCE',
|
|
57
|
-
useValue: {}
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
provide: UtilsService,
|
|
61
|
-
useValue: {}
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
provide: IAMConfigService,
|
|
65
|
-
useValue: mockIamConfigService
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
provide: IAMDataSourceService,
|
|
69
|
-
useValue: mockDataSourceProvider
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
provide: PermissionService,
|
|
73
|
-
useValue: mockPermissionService
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
provide: PermissionCacheService,
|
|
77
|
-
useValue: mockPermissionCacheService
|
|
78
|
-
}
|
|
79
|
-
]
|
|
80
|
-
}).compile();
|
|
81
|
-
service = await module.resolve(ActionService);
|
|
82
|
-
});
|
|
83
|
-
describe('getActionsForPermission', ()=>{
|
|
84
|
-
it('returns all actions when the company feature is disabled', async ()=>{
|
|
85
|
-
mockRepo.find.mockResolvedValue([
|
|
86
|
-
buildAction()
|
|
87
|
-
]);
|
|
88
|
-
const result = await service.getActionsForPermission(buildMockUser());
|
|
89
|
-
expect(mockPermissionService.getCompanyActions).not.toHaveBeenCalled();
|
|
90
|
-
expect(result).toHaveLength(1);
|
|
91
|
-
expect(result[0].code).toBe('products.read');
|
|
92
|
-
});
|
|
93
|
-
it('scopes actions to the company whitelist when the company feature is enabled', async ()=>{
|
|
94
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
95
|
-
mockPermissionService.getCompanyActions.mockResolvedValue([
|
|
96
|
-
'action-uuid-1'
|
|
97
|
-
]);
|
|
98
|
-
mockRepo.find.mockResolvedValue([
|
|
99
|
-
buildAction()
|
|
100
|
-
]);
|
|
101
|
-
await service.getActionsForPermission(buildMockUser(), 'company-uuid-1');
|
|
102
|
-
expect(mockPermissionService.getCompanyActions).toHaveBeenCalledWith('company-uuid-1', true);
|
|
103
|
-
expect(mockRepo.find).toHaveBeenCalledWith(expect.objectContaining({
|
|
104
|
-
where: {
|
|
105
|
-
id: expect.anything()
|
|
106
|
-
}
|
|
107
|
-
}));
|
|
108
|
-
});
|
|
109
|
-
it('short-circuits without querying when the company has no whitelisted actions', async ()=>{
|
|
110
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
111
|
-
mockPermissionService.getCompanyActions.mockResolvedValue([]);
|
|
112
|
-
const result = await service.getActionsForPermission(buildMockUser(), 'company-uuid-1');
|
|
113
|
-
expect(result).toEqual([]);
|
|
114
|
-
expect(mockRepo.find).not.toHaveBeenCalled();
|
|
115
|
-
});
|
|
116
|
-
it('throws when no user is provided', async ()=>{
|
|
117
|
-
await expect(service.getActionsForPermission(null)).rejects.toThrow();
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
describe('getActionTree', ()=>{
|
|
121
|
-
it('nests child actions under their parent', async ()=>{
|
|
122
|
-
const parent = buildAction({
|
|
123
|
-
id: 'parent-1',
|
|
124
|
-
parentId: null,
|
|
125
|
-
serial: 1
|
|
126
|
-
});
|
|
127
|
-
const child = buildAction({
|
|
128
|
-
id: 'child-1',
|
|
129
|
-
parentId: 'parent-1',
|
|
130
|
-
serial: 2,
|
|
131
|
-
code: 'products.create'
|
|
132
|
-
});
|
|
133
|
-
mockRepo.createQueryBuilder().getMany.mockResolvedValue([
|
|
134
|
-
parent,
|
|
135
|
-
child
|
|
136
|
-
]);
|
|
137
|
-
const tree = await service.getActionTree(buildMockUser());
|
|
138
|
-
expect(tree).toHaveLength(1);
|
|
139
|
-
expect(tree[0].id).toBe('parent-1');
|
|
140
|
-
expect(tree[0].children).toHaveLength(1);
|
|
141
|
-
expect(tree[0].children?.[0].id).toBe('child-1');
|
|
142
|
-
});
|
|
143
|
-
it('throws when no user is provided', async ()=>{
|
|
144
|
-
await expect(service.getActionTree(null)).rejects.toThrow();
|
|
145
|
-
});
|
|
146
|
-
it('treats an action with a missing/unknown parentId as a root node', async ()=>{
|
|
147
|
-
const orphan = buildAction({
|
|
148
|
-
id: 'orphan-1',
|
|
149
|
-
parentId: 'does-not-exist'
|
|
150
|
-
});
|
|
151
|
-
mockRepo.createQueryBuilder().getMany.mockResolvedValue([
|
|
152
|
-
orphan
|
|
153
|
-
]);
|
|
154
|
-
const tree = await service.getActionTree(buildMockUser());
|
|
155
|
-
expect(tree).toHaveLength(1);
|
|
156
|
-
expect(tree[0].id).toBe('orphan-1');
|
|
157
|
-
});
|
|
158
|
-
it('excludes soft-deleted actions by default and applies search/isActive filters', async ()=>{
|
|
159
|
-
const query = mockRepo.createQueryBuilder();
|
|
160
|
-
query.getMany.mockResolvedValue([]);
|
|
161
|
-
await service.getActionTree(buildMockUser(), 'view', true, false);
|
|
162
|
-
expect(query.andWhere).toHaveBeenCalledWith('action.deletedAt IS NULL');
|
|
163
|
-
expect(query.andWhere).toHaveBeenCalledWith('action.isActive = :isActive', {
|
|
164
|
-
isActive: true
|
|
165
|
-
});
|
|
166
|
-
expect(query.andWhere).toHaveBeenCalledWith('(action.name LIKE :search OR action.code LIKE :search)', {
|
|
167
|
-
search: '%view%'
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
it('includes soft-deleted actions when withDeleted is true', async ()=>{
|
|
171
|
-
const query = mockRepo.createQueryBuilder();
|
|
172
|
-
query.getMany.mockResolvedValue([]);
|
|
173
|
-
await service.getActionTree(buildMockUser(), undefined, undefined, true);
|
|
174
|
-
expect(query.andWhere).not.toHaveBeenCalledWith('action.deletedAt IS NULL');
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
describe('getSelectQuery', ()=>{
|
|
178
|
-
it('selects the default field set when none is provided', async ()=>{
|
|
179
|
-
const query = mockRepo.createQueryBuilder();
|
|
180
|
-
await service.getSelectQuery(query, buildMockUser());
|
|
181
|
-
expect(query.select).toHaveBeenCalledWith(expect.arrayContaining([
|
|
182
|
-
'action.code',
|
|
183
|
-
'action.actionType',
|
|
184
|
-
'action.permissionLogic'
|
|
185
|
-
]));
|
|
186
|
-
});
|
|
187
|
-
it('uses the explicitly provided select list as-is', async ()=>{
|
|
188
|
-
const query = mockRepo.createQueryBuilder();
|
|
189
|
-
await service.getSelectQuery(query, buildMockUser(), [
|
|
190
|
-
'id',
|
|
191
|
-
'code'
|
|
192
|
-
]);
|
|
193
|
-
expect(query.select).toHaveBeenCalledWith([
|
|
194
|
-
'action.id',
|
|
195
|
-
'action.code'
|
|
196
|
-
]);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
describe('getGlobalSearchQuery', ()=>{
|
|
200
|
-
it('filters by name, code, or description', async ()=>{
|
|
201
|
-
const query = mockRepo.createQueryBuilder();
|
|
202
|
-
await service.getGlobalSearchQuery(query, 'view');
|
|
203
|
-
expect(query.andWhere).toHaveBeenCalledWith('(action.name LIKE :search OR action.code LIKE :search OR action.description LIKE :search)', {
|
|
204
|
-
search: '%view%'
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
describe('convertEntityToResponseDto', ()=>{
|
|
209
|
-
it('maps every entity field onto the response shape', ()=>{
|
|
210
|
-
const entity = buildAction({
|
|
211
|
-
code: 'user.view'
|
|
212
|
-
});
|
|
213
|
-
const dto = service.convertEntityToResponseDto(entity, false);
|
|
214
|
-
expect(dto).toEqual(expect.objectContaining({
|
|
215
|
-
id: entity.id,
|
|
216
|
-
code: 'user.view',
|
|
217
|
-
actionType: ActionType.BACKEND
|
|
218
|
-
}));
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
describe('beforeDeleteOperation', ()=>{
|
|
222
|
-
function buildQueryRunner(permissionRepo) {
|
|
223
|
-
return {
|
|
224
|
-
manager: {
|
|
225
|
-
getRepository: jest.fn().mockReturnValue(permissionRepo)
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
it('does nothing when the delete DTO has no ids', async ()=>{
|
|
230
|
-
const permissionRepo = createMockRepository();
|
|
231
|
-
const queryRunner = buildQueryRunner(permissionRepo);
|
|
232
|
-
await service.beforeDeleteOperation({
|
|
233
|
-
id: [],
|
|
234
|
-
type: 'delete'
|
|
235
|
-
}, buildMockUser(), queryRunner);
|
|
236
|
-
expect(permissionRepo.find).not.toHaveBeenCalled();
|
|
237
|
-
});
|
|
238
|
-
it('cascades role/user/company action-permission cleanup and invalidates affected caches', async ()=>{
|
|
239
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
240
|
-
const permissionRepo = createMockRepository();
|
|
241
|
-
// Company feature is disabled, so companyActionRows resolves via Promise.resolve([]) and
|
|
242
|
-
// never calls permissionRepo.find() — only 3 real find() calls happen: role-action,
|
|
243
|
-
// user-action, then the role -> user cascade lookup.
|
|
244
|
-
permissionRepo.find.mockResolvedValueOnce([
|
|
245
|
-
{
|
|
246
|
-
sourceId: 'role-1'
|
|
247
|
-
}
|
|
248
|
-
]) // role-action rows referencing the deleted action
|
|
249
|
-
.mockResolvedValueOnce([
|
|
250
|
-
{
|
|
251
|
-
sourceId: 'user-1'
|
|
252
|
-
}
|
|
253
|
-
]) // direct user-action rows
|
|
254
|
-
.mockResolvedValueOnce([
|
|
255
|
-
{
|
|
256
|
-
sourceId: 'user-2'
|
|
257
|
-
}
|
|
258
|
-
]); // users holding role-1 (cascaded lookup)
|
|
259
|
-
const queryRunner = buildQueryRunner(permissionRepo);
|
|
260
|
-
await service.beforeDeleteOperation({
|
|
261
|
-
id: 'action-uuid-1',
|
|
262
|
-
type: 'delete'
|
|
263
|
-
}, buildMockUser(), queryRunner);
|
|
264
|
-
expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
265
|
-
permissionType: 'role_action'
|
|
266
|
-
}));
|
|
267
|
-
expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
268
|
-
permissionType: 'user_action'
|
|
269
|
-
}));
|
|
270
|
-
expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
271
|
-
permissionType: 'company_action'
|
|
272
|
-
}));
|
|
273
|
-
expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith(expect.arrayContaining([
|
|
274
|
-
'user-1',
|
|
275
|
-
'user-2'
|
|
276
|
-
]));
|
|
277
|
-
expect(mockPermissionCacheService.invalidateCompanyMembersCache).not.toHaveBeenCalled();
|
|
278
|
-
});
|
|
279
|
-
it('also queries and invalidates company whitelist entries when the company feature is enabled', async ()=>{
|
|
280
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
281
|
-
const permissionRepo = createMockRepository();
|
|
282
|
-
permissionRepo.find.mockResolvedValueOnce([]) // role-action rows
|
|
283
|
-
.mockResolvedValueOnce([]) // direct user-action rows
|
|
284
|
-
.mockResolvedValueOnce([
|
|
285
|
-
{
|
|
286
|
-
sourceId: 'company-1'
|
|
287
|
-
}
|
|
288
|
-
]); // company-action rows
|
|
289
|
-
const queryRunner = buildQueryRunner(permissionRepo);
|
|
290
|
-
await service.beforeDeleteOperation({
|
|
291
|
-
id: [
|
|
292
|
-
'action-uuid-1'
|
|
293
|
-
],
|
|
294
|
-
type: 'delete'
|
|
295
|
-
}, buildMockUser(), queryRunner);
|
|
296
|
-
expect(mockPermissionCacheService.invalidateCompanyMembersCache).toHaveBeenCalledWith('company-1');
|
|
297
|
-
});
|
|
298
|
-
it('does not invalidate any cache when nothing is affected', async ()=>{
|
|
299
|
-
const permissionRepo = createMockRepository();
|
|
300
|
-
permissionRepo.find.mockResolvedValue([]);
|
|
301
|
-
const queryRunner = buildQueryRunner(permissionRepo);
|
|
302
|
-
await service.beforeDeleteOperation({
|
|
303
|
-
id: [
|
|
304
|
-
'action-uuid-1'
|
|
305
|
-
],
|
|
306
|
-
type: 'delete'
|
|
307
|
-
}, buildMockUser(), queryRunner);
|
|
308
|
-
expect(mockPermissionCacheService.invalidateUsers).not.toHaveBeenCalled();
|
|
309
|
-
});
|
|
310
|
-
it('rejects deleting a readonly action', async ()=>{
|
|
311
|
-
mockRepo.find.mockResolvedValue([
|
|
312
|
-
buildAction({
|
|
313
|
-
readOnly: true
|
|
314
|
-
})
|
|
315
|
-
]);
|
|
316
|
-
const permissionRepo = createMockRepository();
|
|
317
|
-
const queryRunner = buildQueryRunner(permissionRepo);
|
|
318
|
-
await expect(service.beforeDeleteOperation({
|
|
319
|
-
id: 'action-uuid-1',
|
|
320
|
-
type: 'delete'
|
|
321
|
-
}, buildMockUser(), queryRunner)).rejects.toThrow('Cannot delete readonly action');
|
|
322
|
-
expect(permissionRepo.find).not.toHaveBeenCalled();
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
describe('beforeUpdateOperation', ()=>{
|
|
326
|
-
it('rejects updating a readonly action', async ()=>{
|
|
327
|
-
mockRepo.find.mockResolvedValue([
|
|
328
|
-
buildAction({
|
|
329
|
-
readOnly: true
|
|
330
|
-
})
|
|
331
|
-
]);
|
|
332
|
-
await expect(service.beforeUpdateOperation({
|
|
333
|
-
id: 'action-uuid-1',
|
|
334
|
-
name: 'Renamed'
|
|
335
|
-
}, buildMockUser(), {})).rejects.toThrow('Cannot modify readonly action');
|
|
336
|
-
});
|
|
337
|
-
it('allows updating a non-readonly action', async ()=>{
|
|
338
|
-
// The service filters readOnly: true in the query itself, so a non-readonly
|
|
339
|
-
// action never comes back — simulate that by resolving an empty array.
|
|
340
|
-
mockRepo.find.mockResolvedValue([]);
|
|
341
|
-
await expect(service.beforeUpdateOperation({
|
|
342
|
-
id: 'action-uuid-1',
|
|
343
|
-
name: 'Renamed'
|
|
344
|
-
}, buildMockUser(), {})).resolves.toBeUndefined();
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
});
|