@hed-hog/core 0.0.301 → 0.0.303
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/dist/integration/services/integration-link.service.d.ts +5 -1
- package/dist/integration/services/integration-link.service.d.ts.map +1 -1
- package/dist/integration/services/integration-link.service.js +141 -53
- package/dist/integration/services/integration-link.service.js.map +1 -1
- package/dist/menu/menu.service.d.ts +1 -0
- package/dist/menu/menu.service.d.ts.map +1 -1
- package/dist/menu/menu.service.js +65 -0
- package/dist/menu/menu.service.js.map +1 -1
- package/hedhog/frontend/app/menu/page.tsx.ejs +1924 -751
- package/hedhog/frontend/app/roles/menus.tsx.ejs +45 -28
- package/hedhog/frontend/app/roles/routes.tsx.ejs +21 -16
- package/hedhog/frontend/app/roles/users.tsx.ejs +22 -14
- package/hedhog/frontend/app/users/permissions.tsx.ejs +7 -9
- package/hedhog/frontend/messages/en.json +28 -1
- package/hedhog/frontend/messages/pt.json +28 -1
- package/hedhog/table/role_menu.yaml +0 -2
- package/package.json +5 -5
- package/src/integration/services/integration-link.service.ts +190 -55
- package/src/menu/menu.service.ts +100 -5
|
@@ -14,13 +14,81 @@ export interface CreateIntegrationLinkDto {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface IntegrationLinkPersistenceClient {
|
|
17
|
-
integrationLink
|
|
17
|
+
integrationLink?: PrismaService['integrationLink'];
|
|
18
|
+
integration_link?: PrismaService['integration_link'];
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
type IntegrationLinkRecord = {
|
|
22
|
+
id: number | string;
|
|
23
|
+
source_module?: string;
|
|
24
|
+
source_entity_type?: string;
|
|
25
|
+
source_entity_id?: string;
|
|
26
|
+
target_module?: string;
|
|
27
|
+
target_entity_type?: string;
|
|
28
|
+
target_entity_id?: string;
|
|
29
|
+
link_type?: LinkType | string;
|
|
30
|
+
created_at?: Date;
|
|
31
|
+
updated_at?: Date;
|
|
32
|
+
sourceModule?: string;
|
|
33
|
+
sourceEntityType?: string;
|
|
34
|
+
sourceEntityId?: string;
|
|
35
|
+
targetModule?: string;
|
|
36
|
+
targetEntityType?: string;
|
|
37
|
+
targetEntityId?: string;
|
|
38
|
+
linkType?: LinkType | string;
|
|
39
|
+
metadata?: Record<string, any> | null;
|
|
40
|
+
createdAt?: Date;
|
|
41
|
+
updatedAt?: Date;
|
|
42
|
+
};
|
|
43
|
+
|
|
20
44
|
@Injectable()
|
|
21
45
|
export class IntegrationLinkService {
|
|
22
46
|
constructor(private readonly prisma: PrismaService) {}
|
|
23
47
|
|
|
48
|
+
private resolvePersistence(
|
|
49
|
+
persistenceClient?: IntegrationLinkPersistenceClient,
|
|
50
|
+
) {
|
|
51
|
+
const client = (persistenceClient ?? this.prisma) as PrismaService &
|
|
52
|
+
IntegrationLinkPersistenceClient;
|
|
53
|
+
const delegate = client.integration_link ?? client.integrationLink;
|
|
54
|
+
const useSnakeCase = !!client.integration_link;
|
|
55
|
+
|
|
56
|
+
if (!delegate) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'Integration link delegate is not available on the Prisma client.',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { delegate, useSnakeCase };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private toDomainLink(record: IntegrationLinkRecord): IntegrationLink {
|
|
66
|
+
return {
|
|
67
|
+
id: String(record.id),
|
|
68
|
+
sourceModule: record.source_module ?? record.sourceModule ?? '',
|
|
69
|
+
sourceEntityType:
|
|
70
|
+
record.source_entity_type ?? record.sourceEntityType ?? '',
|
|
71
|
+
sourceEntityId: record.source_entity_id ?? record.sourceEntityId ?? '',
|
|
72
|
+
targetModule: record.target_module ?? record.targetModule ?? '',
|
|
73
|
+
targetEntityType:
|
|
74
|
+
record.target_entity_type ?? record.targetEntityType ?? '',
|
|
75
|
+
targetEntityId: record.target_entity_id ?? record.targetEntityId ?? '',
|
|
76
|
+
linkType: (record.link_type ?? record.linkType ?? LinkType.REFERENCE) as LinkType,
|
|
77
|
+
metadata: record.metadata ?? null,
|
|
78
|
+
createdAt: record.created_at ?? record.createdAt ?? new Date(),
|
|
79
|
+
updatedAt: record.updated_at ?? record.updatedAt ?? new Date(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private toDatabaseId(linkId: string | number) {
|
|
84
|
+
if (typeof linkId === 'number') {
|
|
85
|
+
return linkId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const numericId = Number(linkId);
|
|
89
|
+
return Number.isNaN(numericId) ? linkId : numericId;
|
|
90
|
+
}
|
|
91
|
+
|
|
24
92
|
/**
|
|
25
93
|
* Create a link between entities from different modules
|
|
26
94
|
*/
|
|
@@ -28,20 +96,33 @@ export class IntegrationLinkService {
|
|
|
28
96
|
dto: CreateIntegrationLinkDto,
|
|
29
97
|
persistenceClient?: IntegrationLinkPersistenceClient,
|
|
30
98
|
): Promise<IntegrationLink> {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
data:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
99
|
+
const { delegate, useSnakeCase } = this.resolvePersistence(persistenceClient);
|
|
100
|
+
|
|
101
|
+
const record = await delegate.create({
|
|
102
|
+
data: useSnakeCase
|
|
103
|
+
? {
|
|
104
|
+
source_module: dto.sourceModule,
|
|
105
|
+
source_entity_type: dto.sourceEntityType,
|
|
106
|
+
source_entity_id: dto.sourceEntityId,
|
|
107
|
+
target_module: dto.targetModule,
|
|
108
|
+
target_entity_type: dto.targetEntityType,
|
|
109
|
+
target_entity_id: dto.targetEntityId,
|
|
110
|
+
link_type: dto.linkType,
|
|
111
|
+
metadata: dto.metadata || null,
|
|
112
|
+
}
|
|
113
|
+
: {
|
|
114
|
+
sourceModule: dto.sourceModule,
|
|
115
|
+
sourceEntityType: dto.sourceEntityType,
|
|
116
|
+
sourceEntityId: dto.sourceEntityId,
|
|
117
|
+
targetModule: dto.targetModule,
|
|
118
|
+
targetEntityType: dto.targetEntityType,
|
|
119
|
+
targetEntityId: dto.targetEntityId,
|
|
120
|
+
linkType: dto.linkType,
|
|
121
|
+
metadata: dto.metadata || null,
|
|
122
|
+
},
|
|
44
123
|
});
|
|
124
|
+
|
|
125
|
+
return this.toDomainLink(record as IntegrationLinkRecord);
|
|
45
126
|
}
|
|
46
127
|
|
|
47
128
|
/**
|
|
@@ -52,13 +133,24 @@ export class IntegrationLinkService {
|
|
|
52
133
|
sourceEntityType: string,
|
|
53
134
|
sourceEntityId: string,
|
|
54
135
|
): Promise<IntegrationLink[]> {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
136
|
+
const { delegate, useSnakeCase } = this.resolvePersistence();
|
|
137
|
+
const records = await delegate.findMany({
|
|
138
|
+
where: useSnakeCase
|
|
139
|
+
? {
|
|
140
|
+
source_module: sourceModule,
|
|
141
|
+
source_entity_type: sourceEntityType,
|
|
142
|
+
source_entity_id: sourceEntityId,
|
|
143
|
+
}
|
|
144
|
+
: {
|
|
145
|
+
sourceModule,
|
|
146
|
+
sourceEntityType,
|
|
147
|
+
sourceEntityId,
|
|
148
|
+
},
|
|
61
149
|
});
|
|
150
|
+
|
|
151
|
+
return records.map((record: IntegrationLinkRecord) =>
|
|
152
|
+
this.toDomainLink(record),
|
|
153
|
+
);
|
|
62
154
|
}
|
|
63
155
|
|
|
64
156
|
/**
|
|
@@ -69,40 +161,62 @@ export class IntegrationLinkService {
|
|
|
69
161
|
targetEntityType: string,
|
|
70
162
|
targetEntityId: string,
|
|
71
163
|
): Promise<IntegrationLink[]> {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
164
|
+
const { delegate, useSnakeCase } = this.resolvePersistence();
|
|
165
|
+
const records = await delegate.findMany({
|
|
166
|
+
where: useSnakeCase
|
|
167
|
+
? {
|
|
168
|
+
target_module: targetModule,
|
|
169
|
+
target_entity_type: targetEntityType,
|
|
170
|
+
target_entity_id: targetEntityId,
|
|
171
|
+
}
|
|
172
|
+
: {
|
|
173
|
+
targetModule,
|
|
174
|
+
targetEntityType,
|
|
175
|
+
targetEntityId,
|
|
176
|
+
},
|
|
78
177
|
});
|
|
178
|
+
|
|
179
|
+
return records.map((record: IntegrationLinkRecord) =>
|
|
180
|
+
this.toDomainLink(record),
|
|
181
|
+
);
|
|
79
182
|
}
|
|
80
183
|
|
|
81
184
|
/**
|
|
82
185
|
* Find links of a specific type
|
|
83
186
|
*/
|
|
84
187
|
async findByLinkType(linkType: LinkType): Promise<IntegrationLink[]> {
|
|
85
|
-
|
|
86
|
-
|
|
188
|
+
const { delegate, useSnakeCase } = this.resolvePersistence();
|
|
189
|
+
const records = await delegate.findMany({
|
|
190
|
+
where: useSnakeCase ? { link_type: linkType } : { linkType },
|
|
87
191
|
});
|
|
192
|
+
|
|
193
|
+
return records.map((record: IntegrationLinkRecord) =>
|
|
194
|
+
this.toDomainLink(record),
|
|
195
|
+
);
|
|
88
196
|
}
|
|
89
197
|
|
|
90
198
|
/**
|
|
91
199
|
* Delete a link
|
|
92
200
|
*/
|
|
93
201
|
async deleteLink(linkId: string): Promise<IntegrationLink> {
|
|
94
|
-
|
|
95
|
-
|
|
202
|
+
const { delegate } = this.resolvePersistence();
|
|
203
|
+
const record = await delegate.delete({
|
|
204
|
+
where: { id: this.toDatabaseId(linkId) },
|
|
96
205
|
});
|
|
206
|
+
|
|
207
|
+
return this.toDomainLink(record as IntegrationLinkRecord);
|
|
97
208
|
}
|
|
98
209
|
|
|
99
210
|
/**
|
|
100
211
|
* Get link by ID
|
|
101
212
|
*/
|
|
102
213
|
async getById(linkId: string): Promise<IntegrationLink | null> {
|
|
103
|
-
|
|
104
|
-
|
|
214
|
+
const { delegate } = this.resolvePersistence();
|
|
215
|
+
const record = await delegate.findUnique({
|
|
216
|
+
where: { id: this.toDatabaseId(linkId) },
|
|
105
217
|
});
|
|
218
|
+
|
|
219
|
+
return record ? this.toDomainLink(record as IntegrationLinkRecord) : null;
|
|
106
220
|
}
|
|
107
221
|
|
|
108
222
|
/**
|
|
@@ -116,39 +230,60 @@ export class IntegrationLinkService {
|
|
|
116
230
|
entity2Type: string,
|
|
117
231
|
entity2Id: string,
|
|
118
232
|
): Promise<IntegrationLink | null> {
|
|
119
|
-
|
|
120
|
-
|
|
233
|
+
const { delegate, useSnakeCase } = this.resolvePersistence();
|
|
234
|
+
|
|
235
|
+
const record = await delegate.findFirst({
|
|
121
236
|
where: {
|
|
122
|
-
OR:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
237
|
+
OR: useSnakeCase
|
|
238
|
+
? [
|
|
239
|
+
{
|
|
240
|
+
source_module: module1,
|
|
241
|
+
source_entity_type: entity1Type,
|
|
242
|
+
source_entity_id: entity1Id,
|
|
243
|
+
target_module: module2,
|
|
244
|
+
target_entity_type: entity2Type,
|
|
245
|
+
target_entity_id: entity2Id,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
source_module: module2,
|
|
249
|
+
source_entity_type: entity2Type,
|
|
250
|
+
source_entity_id: entity2Id,
|
|
251
|
+
target_module: module1,
|
|
252
|
+
target_entity_type: entity1Type,
|
|
253
|
+
target_entity_id: entity1Id,
|
|
254
|
+
},
|
|
255
|
+
]
|
|
256
|
+
: [
|
|
257
|
+
{
|
|
258
|
+
sourceModule: module1,
|
|
259
|
+
sourceEntityType: entity1Type,
|
|
260
|
+
sourceEntityId: entity1Id,
|
|
261
|
+
targetModule: module2,
|
|
262
|
+
targetEntityType: entity2Type,
|
|
263
|
+
targetEntityId: entity2Id,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
sourceModule: module2,
|
|
267
|
+
sourceEntityType: entity2Type,
|
|
268
|
+
sourceEntityId: entity2Id,
|
|
269
|
+
targetModule: module1,
|
|
270
|
+
targetEntityType: entity1Type,
|
|
271
|
+
targetEntityId: entity1Id,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
140
274
|
},
|
|
141
275
|
});
|
|
142
276
|
|
|
143
|
-
return
|
|
277
|
+
return record ? this.toDomainLink(record as IntegrationLinkRecord) : null;
|
|
144
278
|
}
|
|
145
279
|
|
|
146
280
|
/**
|
|
147
281
|
* Count links from a module
|
|
148
282
|
*/
|
|
149
283
|
async countFromModule(sourceModule: string): Promise<number> {
|
|
150
|
-
|
|
151
|
-
|
|
284
|
+
const { delegate, useSnakeCase } = this.resolvePersistence();
|
|
285
|
+
return delegate.count({
|
|
286
|
+
where: useSnakeCase ? { source_module: sourceModule } : { sourceModule },
|
|
152
287
|
});
|
|
153
288
|
}
|
|
154
289
|
}
|
package/src/menu/menu.service.ts
CHANGED
|
@@ -3,11 +3,11 @@ import { getLocaleText } from '@hed-hog/api-locale';
|
|
|
3
3
|
import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
4
4
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
BadRequestException,
|
|
7
|
+
Inject,
|
|
8
|
+
Injectable,
|
|
9
|
+
NotFoundException,
|
|
10
|
+
forwardRef,
|
|
11
11
|
} from '@nestjs/common';
|
|
12
12
|
import { DeleteDTO } from '../dto/delete.dto';
|
|
13
13
|
import { UpdateIdsDTO } from '../dto/update-ids.dto';
|
|
@@ -24,6 +24,67 @@ export class MenuService {
|
|
|
24
24
|
private readonly paginationService: PaginationService,
|
|
25
25
|
) {}
|
|
26
26
|
|
|
27
|
+
private async ensureValidParent(
|
|
28
|
+
locale: string,
|
|
29
|
+
currentMenuId: number | null,
|
|
30
|
+
parentId?: number | null,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
if (parentId == null) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const parent = await this.prismaService.menu.findUnique({
|
|
37
|
+
where: { id: parentId },
|
|
38
|
+
select: { id: true, menu_id: true },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!parent) {
|
|
42
|
+
throw new BadRequestException(
|
|
43
|
+
getLocaleText('menuNotFound', locale, 'Menu not found.'),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (currentMenuId != null && parentId === currentMenuId) {
|
|
48
|
+
throw new BadRequestException(
|
|
49
|
+
getLocaleText(
|
|
50
|
+
'menuInvalidParent',
|
|
51
|
+
locale,
|
|
52
|
+
'A menu cannot be its own parent.',
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (currentMenuId == null) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const visited = new Set<number>([currentMenuId]);
|
|
62
|
+
let cursor: { id: number; menu_id: number | null } | null = parent;
|
|
63
|
+
|
|
64
|
+
while (cursor) {
|
|
65
|
+
if (visited.has(cursor.id)) {
|
|
66
|
+
throw new BadRequestException(
|
|
67
|
+
getLocaleText(
|
|
68
|
+
'menuInvalidParent',
|
|
69
|
+
locale,
|
|
70
|
+
'You cannot move a menu inside itself or one of its descendants.',
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
visited.add(cursor.id);
|
|
76
|
+
|
|
77
|
+
if (cursor.menu_id == null) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
cursor = await this.prismaService.menu.findUnique({
|
|
82
|
+
where: { id: cursor.menu_id },
|
|
83
|
+
select: { id: true, menu_id: true },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
27
88
|
async updateScreens(locale:string,menuId: number, data: UpdateIdsDTO): Promise<{count:number}> {
|
|
28
89
|
|
|
29
90
|
const menuExists = await this.prismaService.menu.count({
|
|
@@ -170,6 +231,26 @@ export class MenuService {
|
|
|
170
231
|
menu_id: true,
|
|
171
232
|
},
|
|
172
233
|
},
|
|
234
|
+
role_user: {
|
|
235
|
+
select: {
|
|
236
|
+
user_id: true,
|
|
237
|
+
user: {
|
|
238
|
+
select: {
|
|
239
|
+
id: true,
|
|
240
|
+
name: true,
|
|
241
|
+
user_identifier: {
|
|
242
|
+
where: {
|
|
243
|
+
type: 'email',
|
|
244
|
+
},
|
|
245
|
+
select: {
|
|
246
|
+
value: true,
|
|
247
|
+
},
|
|
248
|
+
take: 1,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
173
254
|
},
|
|
174
255
|
},
|
|
175
256
|
'role_locale',
|
|
@@ -252,6 +333,11 @@ export class MenuService {
|
|
|
252
333
|
where: { locale: { code: locale } },
|
|
253
334
|
select: { name: true },
|
|
254
335
|
},
|
|
336
|
+
_count: {
|
|
337
|
+
select: {
|
|
338
|
+
role_menu: true,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
255
341
|
},
|
|
256
342
|
});
|
|
257
343
|
return menus.map((m: any) => itemTranslations('menu_locale', m));
|
|
@@ -293,6 +379,11 @@ export class MenuService {
|
|
|
293
379
|
name: true,
|
|
294
380
|
},
|
|
295
381
|
},
|
|
382
|
+
_count: {
|
|
383
|
+
select: {
|
|
384
|
+
role_menu: true,
|
|
385
|
+
},
|
|
386
|
+
},
|
|
296
387
|
},
|
|
297
388
|
},
|
|
298
389
|
'menu_locale',
|
|
@@ -323,6 +414,8 @@ export class MenuService {
|
|
|
323
414
|
}
|
|
324
415
|
|
|
325
416
|
async create(_locale: string, { slug, url, icon, order, menu_id, locale }: CreateDTO) {
|
|
417
|
+
await this.ensureValidParent(_locale, null, menu_id);
|
|
418
|
+
|
|
326
419
|
const created = await this.prismaService.menu.create({
|
|
327
420
|
data: { slug, url, icon, order, menu_id },
|
|
328
421
|
});
|
|
@@ -355,6 +448,8 @@ export class MenuService {
|
|
|
355
448
|
},
|
|
356
449
|
});
|
|
357
450
|
|
|
451
|
+
await this.ensureValidParent(locale, id, data.menu_id);
|
|
452
|
+
|
|
358
453
|
if (!menuExists) {
|
|
359
454
|
throw new BadRequestException(
|
|
360
455
|
getLocaleText('menuNotFound', locale, 'Menu not found.'),
|