@solidxai/core 0.1.11-beta.0 → 0.1.11-beta.2
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/CLAUDE.md +26 -0
- package/dist/controllers/service.controller.d.ts +1 -0
- package/dist/controllers/service.controller.d.ts.map +1 -1
- package/dist/controllers/service.controller.js +2 -2
- package/dist/controllers/service.controller.js.map +1 -1
- package/dist/helpers/field-crud-managers/IntFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/IntFieldCrudManager.js +4 -1
- package/dist/helpers/field-crud-managers/IntFieldCrudManager.js.map +1 -1
- package/dist/helpers/solid-registry.d.ts +2 -1
- package/dist/helpers/solid-registry.d.ts.map +1 -1
- package/dist/helpers/solid-registry.js.map +1 -1
- package/dist/repository/scheduled-job.repository.d.ts +3 -0
- package/dist/repository/scheduled-job.repository.d.ts.map +1 -1
- package/dist/repository/scheduled-job.repository.js +60 -0
- package/dist/repository/scheduled-job.repository.js.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts +37 -2
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +1071 -232
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +7 -0
- package/dist/services/action-metadata.service.d.ts +7 -0
- package/dist/services/action-metadata.service.d.ts.map +1 -1
- package/dist/services/action-metadata.service.js +76 -3
- package/dist/services/action-metadata.service.js.map +1 -1
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.d.ts.map +1 -1
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js +6 -5
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js.map +1 -1
- package/dist/services/locale.service.d.ts +14 -0
- package/dist/services/locale.service.d.ts.map +1 -1
- package/dist/services/locale.service.js +83 -0
- package/dist/services/locale.service.js.map +1 -1
- package/dist/services/role-metadata.service.d.ts +5 -0
- package/dist/services/role-metadata.service.d.ts.map +1 -1
- package/dist/services/role-metadata.service.js +95 -33
- package/dist/services/role-metadata.service.js.map +1 -1
- package/dist/services/view-metadata.service.d.ts +6 -0
- package/dist/services/view-metadata.service.d.ts.map +1 -1
- package/dist/services/view-metadata.service.js +70 -8
- package/dist/services/view-metadata.service.js.map +1 -1
- package/dist/subscribers/audit.subscriber.d.ts +2 -0
- package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
- package/dist/subscribers/audit.subscriber.js +25 -6
- package/dist/subscribers/audit.subscriber.js.map +1 -1
- package/dist/subscribers/computed-entity-field.subscriber.d.ts.map +1 -1
- package/dist/subscribers/computed-entity-field.subscriber.js +1 -0
- package/dist/subscribers/computed-entity-field.subscriber.js.map +1 -1
- package/dist/subscribers/created-by-updated-by.subscriber.d.ts.map +1 -1
- package/dist/subscribers/created-by-updated-by.subscriber.js.map +1 -1
- package/dist/subscribers/field-metadata.subscriber.d.ts.map +1 -1
- package/dist/subscribers/field-metadata.subscriber.js.map +1 -1
- package/package.json +1 -1
- package/src/controllers/service.controller.ts +3 -2
- package/src/helpers/field-crud-managers/IntFieldCrudManager.ts +4 -1
- package/src/helpers/solid-registry.ts +7 -1
- package/src/repository/scheduled-job.repository.ts +73 -0
- package/src/seeders/module-metadata-seeder.service.ts +1295 -264
- package/src/seeders/seed-data/solid-core-metadata.json +7 -0
- package/src/services/action-metadata.service.ts +87 -2
- package/src/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.ts +10 -5
- package/src/services/locale.service.ts +111 -3
- package/src/services/role-metadata.service.ts +112 -49
- package/src/services/view-metadata.service.ts +83 -9
- package/src/subscribers/audit.subscriber.ts +34 -14
- package/src/subscribers/computed-entity-field.subscriber.ts +1 -0
- package/src/subscribers/created-by-updated-by.subscriber.ts +3 -0
- package/src/subscribers/field-metadata.subscriber.ts +3 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common';
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
2
2
|
import { ModuleRef } from "@nestjs/core";
|
|
3
3
|
import { InjectEntityManager } from '@nestjs/typeorm';
|
|
4
4
|
import { CRUDService } from 'src/services/crud.service';
|
|
@@ -10,6 +10,79 @@ import { ActionMetadata } from '../entities/action-metadata.entity';
|
|
|
10
10
|
|
|
11
11
|
@Injectable()
|
|
12
12
|
export class ActionMetadataService extends CRUDService<ActionMetadata> {
|
|
13
|
+
private readonly logger = new Logger(ActionMetadataService.name);
|
|
14
|
+
private readonly comparableFields = [
|
|
15
|
+
'name',
|
|
16
|
+
'displayName',
|
|
17
|
+
'type',
|
|
18
|
+
'domain',
|
|
19
|
+
'context',
|
|
20
|
+
'customComponent',
|
|
21
|
+
'customIsModal',
|
|
22
|
+
'serverEndpoint',
|
|
23
|
+
'module',
|
|
24
|
+
'model',
|
|
25
|
+
'view',
|
|
26
|
+
] as const;
|
|
27
|
+
private readonly relationCompareFields = new Set(['module', 'model', 'view']);
|
|
28
|
+
private readonly jsonCompareFields = new Set(['domain', 'context']);
|
|
29
|
+
|
|
30
|
+
private normalizeJsonFieldValue(value: any): any {
|
|
31
|
+
if (typeof value === 'string') {
|
|
32
|
+
const trimmedValue = value.trim();
|
|
33
|
+
if (
|
|
34
|
+
(trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) ||
|
|
35
|
+
(trimmedValue.startsWith('[') && trimmedValue.endsWith(']'))
|
|
36
|
+
) {
|
|
37
|
+
try {
|
|
38
|
+
return this.normalizeJsonFieldValue(JSON.parse(trimmedValue));
|
|
39
|
+
} catch {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return value.map((item) => this.normalizeJsonFieldValue(item));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (value && typeof value === 'object') {
|
|
51
|
+
return Object.keys(value)
|
|
52
|
+
.sort()
|
|
53
|
+
.reduce((result, key) => {
|
|
54
|
+
result[key] = this.normalizeJsonFieldValue(value[key]);
|
|
55
|
+
return result;
|
|
56
|
+
}, {} as Record<string, any>);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private getCanonicalJsonFieldString(value: any): string {
|
|
63
|
+
return JSON.stringify(this.normalizeJsonFieldValue(value) ?? null);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private hasChanges(existingSolidAction: ActionMetadata, updateSolidActionDto: any): boolean {
|
|
67
|
+
return this.comparableFields.some((key) => {
|
|
68
|
+
const value = updateSolidActionDto[key];
|
|
69
|
+
if (typeof value === 'undefined') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.relationCompareFields.has(key)) {
|
|
74
|
+
const relationValue = value as any;
|
|
75
|
+
return (existingSolidAction as any)[key]?.id !== relationValue?.id;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (this.jsonCompareFields.has(key)) {
|
|
79
|
+
return this.getCanonicalJsonFieldString((existingSolidAction as any)[key]) !== this.getCanonicalJsonFieldString(value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (existingSolidAction as any)[key] !== value;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
13
86
|
constructor(
|
|
14
87
|
@InjectEntityManager()
|
|
15
88
|
readonly entityManager: EntityManager,
|
|
@@ -37,11 +110,23 @@ export class ActionMetadataService extends CRUDService<ActionMetadata> {
|
|
|
37
110
|
const existingSolidAction = await this.repo.findOne({
|
|
38
111
|
where: {
|
|
39
112
|
name: updateSolidActionDto.name
|
|
40
|
-
}
|
|
113
|
+
},
|
|
114
|
+
relations: {
|
|
115
|
+
module: true,
|
|
116
|
+
model: true,
|
|
117
|
+
view: true,
|
|
118
|
+
},
|
|
41
119
|
})
|
|
42
120
|
|
|
43
121
|
// if found
|
|
44
122
|
if (existingSolidAction) {
|
|
123
|
+
const hasChanges = this.hasChanges(existingSolidAction, updateSolidActionDto);
|
|
124
|
+
|
|
125
|
+
if (!hasChanges) {
|
|
126
|
+
this.logger.debug(`Skipping action upsert for ${updateSolidActionDto.name}; no changes detected.`);
|
|
127
|
+
return existingSolidAction;
|
|
128
|
+
}
|
|
129
|
+
|
|
45
130
|
const updatedSolidActionDto = { ...existingSolidAction, ...updateSolidActionDto };
|
|
46
131
|
return this.repo.save(updatedSolidActionDto);
|
|
47
132
|
}
|
package/src/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.ts
CHANGED
|
@@ -45,7 +45,12 @@ export class AlphaNumExternalIdComputationProvider<T extends CommonEntity> imple
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
// Use the originating transaction's EntityManager so the uniqueness check runs on the
|
|
49
|
+
// active connection. Falls back to the injected manager only when no event context is
|
|
50
|
+
// available (e.g. direct invocation outside a TypeORM lifecycle event).
|
|
51
|
+
const manager = eventContext?.manager ?? this.entityManager;
|
|
52
|
+
|
|
53
|
+
const uniqueCode = await this.generateUniqueExternalId(manager, resolvedPrefix, codeLength, computedFieldMetadata.fieldName, entityName);
|
|
49
54
|
const finalExternalId = resolvedPrefix ? `${resolvedPrefix}-${uniqueCode}` : uniqueCode;
|
|
50
55
|
|
|
51
56
|
triggerEntity[computedFieldMetadata.fieldName] = finalExternalId;
|
|
@@ -60,8 +65,8 @@ export class AlphaNumExternalIdComputationProvider<T extends CommonEntity> imple
|
|
|
60
65
|
return result;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
private async isExternalIdUnique(externalId: string, fieldName: string, entityName: string): Promise<boolean> {
|
|
64
|
-
const count = await
|
|
68
|
+
private async isExternalIdUnique(manager: EntityManager, externalId: string, fieldName: string, entityName: string): Promise<boolean> {
|
|
69
|
+
const count = await manager.count(entityName as any,
|
|
65
70
|
{
|
|
66
71
|
where: { [fieldName]: externalId },
|
|
67
72
|
}
|
|
@@ -69,7 +74,7 @@ export class AlphaNumExternalIdComputationProvider<T extends CommonEntity> imple
|
|
|
69
74
|
return count === 0;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
private async generateUniqueExternalId(resolvedPrefix: string, codeLength: number, fieldName: string, entityName: string): Promise<string> {
|
|
77
|
+
private async generateUniqueExternalId(manager: EntityManager, resolvedPrefix: string, codeLength: number, fieldName: string, entityName: string): Promise<string> {
|
|
73
78
|
const maxAttempts = 10;
|
|
74
79
|
|
|
75
80
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
@@ -78,7 +83,7 @@ export class AlphaNumExternalIdComputationProvider<T extends CommonEntity> imple
|
|
|
78
83
|
|
|
79
84
|
const fullId = resolvedPrefix ? `${resolvedPrefix}-${newId}` : newId;
|
|
80
85
|
|
|
81
|
-
const isUnique = await this.isExternalIdUnique(fullId, fieldName, entityName);
|
|
86
|
+
const isUnique = await this.isExternalIdUnique(manager, fullId, fieldName, entityName);
|
|
82
87
|
|
|
83
88
|
if (isUnique) {
|
|
84
89
|
return newId;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
|
1
|
+
import { BadRequestException, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
|
2
2
|
import { ModuleRef } from "@nestjs/core";
|
|
3
3
|
import { InjectEntityManager } from '@nestjs/typeorm';
|
|
4
|
-
import { EntityManager } from 'typeorm';
|
|
4
|
+
import { EntityManager, In, IsNull, Not } from 'typeorm';
|
|
5
5
|
|
|
6
6
|
import { Locale } from 'src/entities/locale.entity';
|
|
7
7
|
import { SolidRegistry } from 'src/helpers/solid-registry';
|
|
@@ -31,7 +31,115 @@ export class LocaleService extends CRUDService<Locale> implements OnApplicationB
|
|
|
31
31
|
// Load the security rules from the database
|
|
32
32
|
this.loadLocales();
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
private async assertCanSetAsDefault(currentLocaleId?: number) {
|
|
36
|
+
const defaultLocales = await this.repo.find({
|
|
37
|
+
where: {
|
|
38
|
+
isDefault: true,
|
|
39
|
+
} as any,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const conflictingDefaultLocale = defaultLocales.find(
|
|
43
|
+
(locale) => locale.id !== currentLocaleId
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (conflictingDefaultLocale) {
|
|
47
|
+
throw new BadRequestException(
|
|
48
|
+
`Locale "${conflictingDefaultLocale.displayName}" is already marked as default. Mark it as non-default first.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override async create(createDto: any, files: Express.Multer.File[] = [], solidRequestContext: any = {}): Promise<Locale> {
|
|
54
|
+
if (createDto?.isDefault === true) {
|
|
55
|
+
await this.assertCanSetAsDefault();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const savedLocale = await super.create(createDto, files, solidRequestContext);
|
|
59
|
+
await this.loadLocales();
|
|
60
|
+
return savedLocale;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override async insertMany(createDtos: any[], filesArray: Express.Multer.File[][] = [], solidRequestContext: any = {}): Promise<Locale[]> {
|
|
64
|
+
const defaultLocalesInPayload = (createDtos || []).filter((dto) => dto?.isDefault === true);
|
|
65
|
+
|
|
66
|
+
if (defaultLocalesInPayload.length > 1) {
|
|
67
|
+
throw new BadRequestException(`Only one locale can be marked as default.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (defaultLocalesInPayload.length === 1) {
|
|
71
|
+
await this.assertCanSetAsDefault();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const savedLocales = await super.insertMany(createDtos, filesArray, solidRequestContext);
|
|
75
|
+
await this.loadLocales();
|
|
76
|
+
return savedLocales;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override async update(id: number, updateDto: any, files: Express.Multer.File[] = [], isPartialUpdate: boolean = false, solidRequestContext: any = {}, isUpdate: boolean = false): Promise<Locale> {
|
|
80
|
+
if (updateDto?.isDefault === true) {
|
|
81
|
+
await this.assertCanSetAsDefault(id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const savedLocale = await super.update(id, updateDto, files, isPartialUpdate, solidRequestContext, isUpdate);
|
|
85
|
+
await this.loadLocales();
|
|
86
|
+
return savedLocale;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override async recover(id: number, solidRequestContext: any = {}) {
|
|
90
|
+
const localeToRecover = await this.repo.findOne({
|
|
91
|
+
where: {
|
|
92
|
+
id,
|
|
93
|
+
deletedAt: Not(IsNull()),
|
|
94
|
+
} as any,
|
|
95
|
+
withDeleted: true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (localeToRecover?.isDefault) {
|
|
99
|
+
await this.assertCanSetAsDefault(id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const recoveredLocale = await super.recover(id, solidRequestContext);
|
|
103
|
+
await this.loadLocales();
|
|
104
|
+
return recoveredLocale;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
override async recoverMany(ids: number[], solidRequestContext: any = {}) {
|
|
108
|
+
const localesToRecover = await this.repo.find({
|
|
109
|
+
where: {
|
|
110
|
+
id: In(ids),
|
|
111
|
+
deletedAt: Not(IsNull()),
|
|
112
|
+
} as any,
|
|
113
|
+
withDeleted: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const defaultLocalesToRecover = localesToRecover.filter((locale) => locale.isDefault);
|
|
117
|
+
if (defaultLocalesToRecover.length > 1) {
|
|
118
|
+
throw new BadRequestException(`Only one locale can be marked as default.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (defaultLocalesToRecover.length === 1) {
|
|
122
|
+
await this.assertCanSetAsDefault(defaultLocalesToRecover[0].id);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const recoveredLocales = await super.recoverMany(ids, solidRequestContext);
|
|
126
|
+
await this.loadLocales();
|
|
127
|
+
return recoveredLocales;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
override async delete(id: number, solidRequestContext: any = {}) {
|
|
131
|
+
const deletedLocale = await super.delete(id, solidRequestContext);
|
|
132
|
+
await this.loadLocales();
|
|
133
|
+
return deletedLocale;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
override async deleteMany(ids: number[], solidRequestContext: any = {}) {
|
|
137
|
+
const deletedLocales = await super.deleteMany(ids, solidRequestContext);
|
|
138
|
+
await this.loadLocales();
|
|
139
|
+
return deletedLocales;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async loadLocales() {
|
|
35
143
|
const locales = await this.repo.find();
|
|
36
144
|
this.logger.debug(`Loaded ${locales.length} locales into registry`);
|
|
37
145
|
this.solidRegistry.registerlocales(locales);
|
|
@@ -8,12 +8,14 @@ import { ERROR_MESSAGES } from 'src/constants/error-messages';
|
|
|
8
8
|
import { PermissionMetadataRepository } from 'src/repository/permission-metadata.repository';
|
|
9
9
|
import { RoleMetadataRepository } from 'src/repository/role-metadata.repository';
|
|
10
10
|
import { CreateRoleMetadataDto } from '../dtos/create-role-metadata.dto';
|
|
11
|
+
import { ModuleMetadata } from '../entities/module-metadata.entity';
|
|
11
12
|
import { PermissionMetadata } from '../entities/permission-metadata.entity';
|
|
12
13
|
import { RoleMetadata } from '../entities/role-metadata.entity';
|
|
13
14
|
|
|
14
15
|
@Injectable()
|
|
15
16
|
export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
16
17
|
private readonly logger = new Logger(RoleMetadataService.name);
|
|
18
|
+
private readonly permissionAssignmentBatchSize = 500;
|
|
17
19
|
|
|
18
20
|
constructor(
|
|
19
21
|
@InjectEntityManager()
|
|
@@ -64,13 +66,16 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
64
66
|
for (let id = 0; id < roles.length; id++) {
|
|
65
67
|
try {
|
|
66
68
|
const roleObj = roles[id];
|
|
69
|
+
const module = await this.resolveRoleModule(roleObj);
|
|
67
70
|
// this.logger.log(`Resolving role: ${JSON.stringify(roleObj)}`);
|
|
68
71
|
|
|
69
72
|
const existingRole = await this.repo.findOne({
|
|
70
73
|
where: {
|
|
71
74
|
name: roleObj.name,
|
|
72
75
|
},
|
|
73
|
-
relations: {
|
|
76
|
+
relations: {
|
|
77
|
+
module: true,
|
|
78
|
+
},
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
// Create only if not existing already.
|
|
@@ -86,9 +91,13 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
// const role = this.repo.create({ ...roleObj, permissions });
|
|
89
|
-
const role = this.repo.create({ ...roleObj });
|
|
94
|
+
const role = this.repo.create({ ...roleObj, ...(module ? { module } : {}) });
|
|
90
95
|
await this.repo.save(role);
|
|
91
96
|
} else {
|
|
97
|
+
if (module && existingRole.module?.id !== module.id) {
|
|
98
|
+
existingRole.module = module;
|
|
99
|
+
await this.repo.save(existingRole);
|
|
100
|
+
}
|
|
92
101
|
/*
|
|
93
102
|
this.logger.debug(`Role ${roleObj.name} already exists`);
|
|
94
103
|
const existingPermissions = existingRole.permissions.map(permission => permission.name);
|
|
@@ -111,6 +120,22 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
111
120
|
}
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
private async resolveRoleModule(roleObj: CreateRoleMetadataDto): Promise<ModuleMetadata | null> {
|
|
124
|
+
if (roleObj.moduleId) {
|
|
125
|
+
return await this.entityManager.getRepository(ModuleMetadata).findOne({
|
|
126
|
+
where: { id: roleObj.moduleId },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (roleObj.moduleUserKey) {
|
|
131
|
+
return await this.entityManager.getRepository(ModuleMetadata).findOne({
|
|
132
|
+
where: { name: roleObj.moduleUserKey },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
async addAllPermissionsToRole(roleName: string): Promise<RoleMetadata> {
|
|
115
140
|
return await this._addPermissionsToRole(roleName, null);
|
|
116
141
|
}
|
|
@@ -126,66 +151,47 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
126
151
|
private async _addPermissionsToRole(roleName: string, permissionNames: string[]): Promise<RoleMetadata> {
|
|
127
152
|
const role = await this.repo.findOne({
|
|
128
153
|
where: { name: roleName },
|
|
129
|
-
|
|
130
|
-
|
|
154
|
+
select: {
|
|
155
|
+
id: true,
|
|
156
|
+
name: true,
|
|
131
157
|
}
|
|
132
158
|
});
|
|
133
159
|
if (!role) {
|
|
134
160
|
throw new Error(`Role '${roleName}' not found.`);
|
|
135
161
|
}
|
|
136
162
|
|
|
137
|
-
|
|
163
|
+
const joinInfo = this.getRolePermissionJoinInfo();
|
|
164
|
+
const targetPermissions = await this.loadTargetPermissions(permissionNames);
|
|
138
165
|
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
if (!permissionNames || permissionNames.length === 0) {
|
|
167
|
+
await this.entityManager
|
|
168
|
+
.createQueryBuilder()
|
|
169
|
+
.delete()
|
|
170
|
+
.from(joinInfo.tableName)
|
|
171
|
+
.where(`${joinInfo.roleIdColumn} = :roleId`, { roleId: role.id })
|
|
172
|
+
.execute();
|
|
141
173
|
|
|
142
|
-
|
|
143
|
-
if (permissionNames && permissionNames.length != 0) {
|
|
144
|
-
// this.logger.log(`Loading specified permissions.`);
|
|
145
|
-
|
|
146
|
-
newPermissions = await this.permissionRepository.find({ where: { name: In(permissionNames) } });
|
|
147
|
-
if (newPermissions.length !== permissionNames.length) {
|
|
148
|
-
throw new Error(`One or more permissions not found.`);
|
|
149
|
-
}
|
|
174
|
+
await this.insertRolePermissionMappings(role.id, targetPermissions.map((permission) => permission.id), joinInfo);
|
|
150
175
|
}
|
|
151
176
|
else {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for (let i = 0; i < newPermissions.length; i++) {
|
|
167
|
-
const newPermission = newPermissions[i];
|
|
168
|
-
let newPermissionFound = true;
|
|
169
|
-
for (let j = 0; j < role.permissions.length; j++) {
|
|
170
|
-
const existingPermission = role.permissions[j];
|
|
171
|
-
if (existingPermission.name === newPermission.name) {
|
|
172
|
-
newPermissionFound = false;
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (newPermissionFound) {
|
|
178
|
-
role.permissions.push(newPermission);
|
|
179
|
-
}
|
|
177
|
+
const existingRows = await this.entityManager
|
|
178
|
+
.createQueryBuilder()
|
|
179
|
+
.select(joinInfo.permissionIdColumn, 'permissionId')
|
|
180
|
+
.from(joinInfo.tableName, 'role_permission')
|
|
181
|
+
.where(`${joinInfo.roleIdColumn} = :roleId`, { roleId: role.id })
|
|
182
|
+
.getRawMany();
|
|
183
|
+
|
|
184
|
+
const existingPermissionIds = new Set(existingRows.map((row) => Number(row.permissionId)));
|
|
185
|
+
const missingPermissionIds = targetPermissions
|
|
186
|
+
.map((permission) => permission.id)
|
|
187
|
+
.filter((permissionId) => !existingPermissionIds.has(permissionId));
|
|
188
|
+
|
|
189
|
+
if (missingPermissionIds.length > 0) {
|
|
190
|
+
await this.insertRolePermissionMappings(role.id, missingPermissionIds, joinInfo);
|
|
180
191
|
}
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
// else we create a new permissions set.
|
|
184
|
-
else {
|
|
185
|
-
role.permissions = newPermissions;
|
|
186
192
|
}
|
|
187
193
|
|
|
188
|
-
return await this.
|
|
194
|
+
return await this.findRoleByName(roleName);
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
async removePermissionsFromRole(roleName: string, permissionNames: string[]): Promise<RoleMetadata> {
|
|
@@ -220,5 +226,62 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
220
226
|
return existingPermission;
|
|
221
227
|
}
|
|
222
228
|
|
|
229
|
+
private async loadTargetPermissions(permissionNames: string[] | null): Promise<PermissionMetadata[]> {
|
|
230
|
+
if (permissionNames && permissionNames.length !== 0) {
|
|
231
|
+
const uniquePermissionNames = [...new Set(permissionNames)];
|
|
232
|
+
const permissions = await this.permissionRepository.find({ where: { name: In(uniquePermissionNames) } });
|
|
233
|
+
if (permissions.length !== uniquePermissionNames.length) {
|
|
234
|
+
throw new Error(`One or more permissions not found.`);
|
|
235
|
+
}
|
|
236
|
+
return permissions;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const permissions = await this.permissionRepository.find();
|
|
240
|
+
if (permissions.length === 0) {
|
|
241
|
+
throw new Error(`No permissions configured in the system. Did you forget to run the PermissionSeederService?`);
|
|
242
|
+
}
|
|
243
|
+
return permissions;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private getRolePermissionJoinInfo(): { tableName: string; roleIdColumn: string; permissionIdColumn: string } {
|
|
247
|
+
const relation = this.repo.metadata.relations.find((candidate) => candidate.propertyName === 'permissions');
|
|
248
|
+
if (!relation?.junctionEntityMetadata) {
|
|
249
|
+
throw new Error('Role-permission join table metadata not found.');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const roleIdColumn = relation.joinColumns[0]?.databaseName;
|
|
253
|
+
const permissionIdColumn = relation.inverseJoinColumns[0]?.databaseName;
|
|
254
|
+
if (!roleIdColumn || !permissionIdColumn) {
|
|
255
|
+
throw new Error('Role-permission join table column metadata not found.');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
tableName: relation.junctionEntityMetadata.tableName,
|
|
260
|
+
roleIdColumn,
|
|
261
|
+
permissionIdColumn,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async insertRolePermissionMappings(
|
|
266
|
+
roleId: number,
|
|
267
|
+
permissionIds: number[],
|
|
268
|
+
joinInfo: { tableName: string; roleIdColumn: string; permissionIdColumn: string },
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
for (let index = 0; index < permissionIds.length; index += this.permissionAssignmentBatchSize) {
|
|
271
|
+
const batch = permissionIds.slice(index, index + this.permissionAssignmentBatchSize);
|
|
272
|
+
await this.entityManager
|
|
273
|
+
.createQueryBuilder()
|
|
274
|
+
.insert()
|
|
275
|
+
.into(joinInfo.tableName)
|
|
276
|
+
.values(
|
|
277
|
+
batch.map((permissionId) => ({
|
|
278
|
+
[joinInfo.roleIdColumn]: roleId,
|
|
279
|
+
[joinInfo.permissionIdColumn]: permissionId,
|
|
280
|
+
})),
|
|
281
|
+
)
|
|
282
|
+
.execute();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
223
286
|
|
|
224
287
|
}
|
|
@@ -21,6 +21,74 @@ import { MenuItemMetadata } from 'src/entities/menu-item-metadata.entity';
|
|
|
21
21
|
|
|
22
22
|
@Injectable()
|
|
23
23
|
export class ViewMetadataService extends CRUDService<ViewMetadata> {
|
|
24
|
+
private readonly comparableFields = [
|
|
25
|
+
'name',
|
|
26
|
+
'displayName',
|
|
27
|
+
'type',
|
|
28
|
+
'context',
|
|
29
|
+
'layout',
|
|
30
|
+
'module',
|
|
31
|
+
'model',
|
|
32
|
+
] as const;
|
|
33
|
+
private readonly relationCompareFields = new Set(['module', 'model']);
|
|
34
|
+
private readonly jsonCompareFields = new Set(['layout', 'context']);
|
|
35
|
+
|
|
36
|
+
private normalizeJsonFieldValue(value: any): any {
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
const trimmedValue = value.trim();
|
|
39
|
+
if (
|
|
40
|
+
(trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) ||
|
|
41
|
+
(trimmedValue.startsWith('[') && trimmedValue.endsWith(']'))
|
|
42
|
+
) {
|
|
43
|
+
try {
|
|
44
|
+
return this.normalizeJsonFieldValue(JSON.parse(trimmedValue));
|
|
45
|
+
} catch {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value.map((item) => this.normalizeJsonFieldValue(item));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (value && typeof value === 'object') {
|
|
57
|
+
return Object.keys(value)
|
|
58
|
+
.sort()
|
|
59
|
+
.reduce((result, key) => {
|
|
60
|
+
result[key] = this.normalizeJsonFieldValue(value[key]);
|
|
61
|
+
return result;
|
|
62
|
+
}, {} as Record<string, any>);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private getCanonicalJsonFieldString(value: any): string {
|
|
69
|
+
return JSON.stringify(this.normalizeJsonFieldValue(value) ?? null);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private hasChanges(existingSolidView: ViewMetadata, updateSolidViewDto: UpdateViewMetadataDto): boolean {
|
|
73
|
+
return this.comparableFields.some((key) => {
|
|
74
|
+
const value = (updateSolidViewDto as any)[key];
|
|
75
|
+
if (typeof value === 'undefined') {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.relationCompareFields.has(key)) {
|
|
80
|
+
const relationValue = value as any;
|
|
81
|
+
return (existingSolidView as any)[key]?.id !== relationValue?.id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (this.jsonCompareFields.has(key)) {
|
|
85
|
+
return this.getCanonicalJsonFieldString((existingSolidView as any)[key]) !== this.getCanonicalJsonFieldString(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (existingSolidView as any)[key] !== value;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
24
92
|
constructor(
|
|
25
93
|
readonly actionMetadataService: ActionMetadataService,
|
|
26
94
|
readonly menuItemMetadataService: MenuItemMetadataService,
|
|
@@ -83,15 +151,11 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
|
|
|
83
151
|
return { records: [], defaultEntityLocaleId: null };
|
|
84
152
|
}
|
|
85
153
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (entityRecord.localeName === defaultLocale?.locale) {
|
|
90
|
-
defaultEntityLocaleId = entityRecord.id;
|
|
91
|
-
this.logger.debug(`Editing default locale record with id ${defaultEntityLocaleId}`);
|
|
154
|
+
const defaultEntityLocaleId = entityRecord.defaultEntityLocaleId || entityRecord.id;
|
|
155
|
+
if (entityRecord.defaultEntityLocaleId) {
|
|
156
|
+
this.logger.debug(`Editing translated locale record. Translation root id: ${defaultEntityLocaleId}`);
|
|
92
157
|
} else {
|
|
93
|
-
|
|
94
|
-
this.logger.debug(`Editing non-default locale record. DefaultEntityLocaleId: ${defaultEntityLocaleId}`);
|
|
158
|
+
this.logger.debug(`Editing translation root record with id ${defaultEntityLocaleId}`);
|
|
95
159
|
}
|
|
96
160
|
|
|
97
161
|
const records = await currentEntityRepository.find({
|
|
@@ -438,10 +502,20 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
|
|
|
438
502
|
|
|
439
503
|
async upsert(updateSolidViewDto: UpdateViewMetadataDto) {
|
|
440
504
|
// First check if module already exists using name
|
|
441
|
-
const existingSolidView = await this.findOneByUserKey(updateSolidViewDto.name
|
|
505
|
+
const existingSolidView = await this.findOneByUserKey(updateSolidViewDto.name, {
|
|
506
|
+
module: true,
|
|
507
|
+
model: true,
|
|
508
|
+
});
|
|
442
509
|
|
|
443
510
|
// if found
|
|
444
511
|
if (existingSolidView) {
|
|
512
|
+
const hasChanges = this.hasChanges(existingSolidView, updateSolidViewDto);
|
|
513
|
+
|
|
514
|
+
if (!hasChanges) {
|
|
515
|
+
this.logger.debug(`Skipping view upsert for ${updateSolidViewDto.name}; no changes detected.`);
|
|
516
|
+
return existingSolidView;
|
|
517
|
+
}
|
|
518
|
+
|
|
445
519
|
const updatedSolidViewDto = { ...existingSolidView, ...updateSolidViewDto };
|
|
446
520
|
return this.repo.save(updatedSolidViewDto);
|
|
447
521
|
}
|