@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.
@@ -14,13 +14,81 @@ export interface CreateIntegrationLinkDto {
14
14
  }
15
15
 
16
16
  export interface IntegrationLinkPersistenceClient {
17
- integrationLink: PrismaService['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 client = persistenceClient ?? this.prisma;
32
-
33
- return client.integrationLink.create({
34
- data: {
35
- sourceModule: dto.sourceModule,
36
- sourceEntityType: dto.sourceEntityType,
37
- sourceEntityId: dto.sourceEntityId,
38
- targetModule: dto.targetModule,
39
- targetEntityType: dto.targetEntityType,
40
- targetEntityId: dto.targetEntityId,
41
- linkType: dto.linkType,
42
- metadata: dto.metadata || null,
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
- return this.prisma.integrationLink.findMany({
56
- where: {
57
- sourceModule,
58
- sourceEntityType,
59
- sourceEntityId,
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
- return this.prisma.integrationLink.findMany({
73
- where: {
74
- targetModule,
75
- targetEntityType,
76
- targetEntityId,
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
- return this.prisma.integrationLink.findMany({
86
- where: { linkType },
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
- return this.prisma.integrationLink.delete({
95
- where: { id: linkId },
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
- return this.prisma.integrationLink.findUnique({
104
- where: { id: linkId },
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
- // Look for forward or reverse direction
120
- const link = await this.prisma.integrationLink.findFirst({
233
+ const { delegate, useSnakeCase } = this.resolvePersistence();
234
+
235
+ const record = await delegate.findFirst({
121
236
  where: {
122
- OR: [
123
- {
124
- sourceModule: module1,
125
- sourceEntityType: entity1Type,
126
- sourceEntityId: entity1Id,
127
- targetModule: module2,
128
- targetEntityType: entity2Type,
129
- targetEntityId: entity2Id,
130
- },
131
- {
132
- sourceModule: module2,
133
- sourceEntityType: entity2Type,
134
- sourceEntityId: entity2Id,
135
- targetModule: module1,
136
- targetEntityType: entity1Type,
137
- targetEntityId: entity1Id,
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 link;
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
- return this.prisma.integrationLink.count({
151
- where: { sourceModule },
284
+ const { delegate, useSnakeCase } = this.resolvePersistence();
285
+ return delegate.count({
286
+ where: useSnakeCase ? { source_module: sourceModule } : { sourceModule },
152
287
  });
153
288
  }
154
289
  }
@@ -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
- BadRequestException,
7
- Inject,
8
- Injectable,
9
- NotFoundException,
10
- forwardRef,
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.'),