@solidxai/core 0.1.11-beta.1 → 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/settings.local.json +15 -0
- 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/seeders/module-metadata-seeder.service.d.ts +5 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +61 -3
- 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/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 +1 -0
- package/dist/services/role-metadata.service.d.ts.map +1 -1
- package/dist/services/role-metadata.service.js +23 -2
- package/dist/services/role-metadata.service.js.map +1 -1
- package/dist/services/view-metadata.service.d.ts.map +1 -1
- package/dist/services/view-metadata.service.js +4 -7
- 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/seeders/module-metadata-seeder.service.ts +72 -2
- package/src/seeders/seed-data/solid-core-metadata.json +7 -0
- package/src/services/1.js +6 -0
- 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 +26 -2
- package/src/services/view-metadata.service.ts +4 -8
- 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
- package/dist-tests/api/authenticate.spec.js +0 -119
- package/dist-tests/api/authenticate.spec.js.map +0 -1
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
- package/dist-tests/api/ping.spec.js +0 -21
- package/dist-tests/api/ping.spec.js.map +0 -1
- package/dist-tests/helpers/auth.js +0 -41
- package/dist-tests/helpers/auth.js.map +0 -1
- package/dist-tests/helpers/env.js +0 -11
- package/dist-tests/helpers/env.js.map +0 -1
- package/docs/agent-hub-grooming.md +0 -301
- package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
- package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
- package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
- package/docs/datasource-introspection-ddl-analysis.md +0 -326
- package/docs/datasource-introspection-implementation-plan.md +0 -306
- package/docs/grouping-enhancements.md +0 -89
- package/docs/java-spring/README.md +0 -3
- package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
- package/docs/module-package-import-handoff.md +0 -691
- package/docs/seed-changes.md +0 -65
- package/docs/test-data-workflow.md +0 -200
- package/docs/type-declaration-import-issue.md +0 -24
|
@@ -39,12 +39,14 @@ import { SignUpDto } from '../dtos/sign-up.dto';
|
|
|
39
39
|
import { ADMIN_ROLE_NAME, CreateRoleMetadataDto } from 'src/dtos/create-role-metadata.dto';
|
|
40
40
|
import { CreateSavedFiltersDto } from 'src/dtos/create-saved-filters.dto';
|
|
41
41
|
import { CreateScheduledJobDto } from 'src/dtos/create-scheduled-job.dto';
|
|
42
|
+
import { CreateLocaleDto } from 'src/dtos/create-locale.dto';
|
|
42
43
|
import { PermissionMetadataRepository } from 'src/repository/permission-metadata.repository';
|
|
43
44
|
import { SavedFiltersRepository } from 'src/repository/saved-filters.repository';
|
|
44
45
|
import { ScheduledJobRepository } from 'src/repository/scheduled-job.repository';
|
|
45
46
|
import { SettingRepository } from 'src/repository/setting.repository';
|
|
46
47
|
import { CreateModelSequenceDto } from 'src/dtos/create-model-sequence.dto';
|
|
47
48
|
import { ModelSequenceRepository } from 'src/repository/model-sequence.repository';
|
|
49
|
+
import { LocaleRepository } from 'src/repository/locale.repository';
|
|
48
50
|
import { ModelSequence } from 'src/entities/model-sequence.entity';
|
|
49
51
|
import { SavedFilters } from 'src/entities/saved-filters.entity';
|
|
50
52
|
import { ScheduledJob } from 'src/entities/scheduled-job.entity';
|
|
@@ -116,6 +118,7 @@ export class ModuleMetadataSeederService {
|
|
|
116
118
|
readonly savedFiltersRepo: SavedFiltersRepository,
|
|
117
119
|
readonly dataSource: DataSource,
|
|
118
120
|
readonly modelSequenceRepo: ModelSequenceRepository,
|
|
121
|
+
readonly localeRepo: LocaleRepository,
|
|
119
122
|
) { }
|
|
120
123
|
|
|
121
124
|
async seed(conf?: any) {
|
|
@@ -194,6 +197,11 @@ export class ModuleMetadataSeederService {
|
|
|
194
197
|
const moduleModelFieldCounts = await this.timeOperation('seed-module-model-fields', () => this.seedModuleModelFields(moduleMetadata), { moduleName: moduleMetadata.name, component: 'module-model-fields' });
|
|
195
198
|
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Module/Model/Fields', moduleModelFieldCounts)}`);
|
|
196
199
|
|
|
200
|
+
currentStep = 'seedLocales';
|
|
201
|
+
this.logger.log(`Seeding Locales`);
|
|
202
|
+
const localeCounts = await this.seedLocales(overallMetadata);
|
|
203
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Locales', localeCounts)}`);
|
|
204
|
+
|
|
197
205
|
currentStep = 'seedPermissions';
|
|
198
206
|
this.logger.log(`Seeding Permissions`);
|
|
199
207
|
const permissionCounts = await this.timeOperation('seed-permissions', () => this.seedPermissions(overallMetadata), { moduleName: moduleMetadata.name, component: 'permissions' });
|
|
@@ -466,6 +474,22 @@ export class ModuleMetadataSeederService {
|
|
|
466
474
|
return { pruned, upserted: listOfValues.length };
|
|
467
475
|
}
|
|
468
476
|
|
|
477
|
+
private async seedLocales(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
478
|
+
this.logger.debug(`[Start] Processing locales`);
|
|
479
|
+
const locales = this.getSeedArray<CreateLocaleDto>(overallMetadata?.locales);
|
|
480
|
+
const existingLocales = await this.localeRepo.find();
|
|
481
|
+
|
|
482
|
+
if (existingLocales.length > 0) {
|
|
483
|
+
this.solidRegistry.registerlocales(existingLocales);
|
|
484
|
+
this.logger.debug(`[End] Skipping locale seed because locales already exist`);
|
|
485
|
+
return { pruned: 0, upserted: 0 };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await this.handleSeedLocales(locales);
|
|
489
|
+
this.logger.debug(`[End] Processing locales`);
|
|
490
|
+
return { pruned: 0, upserted: locales.length };
|
|
491
|
+
}
|
|
492
|
+
|
|
469
493
|
private async setupDefaultRolesWithPermissions() {
|
|
470
494
|
this.logger.debug(`About to add all permissions to the Admin role`);
|
|
471
495
|
await this.timeOperation('role-add-all-permissions', () => this.roleService.addAllPermissionsToRole(ADMIN_ROLE_NAME), {
|
|
@@ -592,11 +616,15 @@ export class ModuleMetadataSeederService {
|
|
|
592
616
|
// OK
|
|
593
617
|
private async seedRoles(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
594
618
|
const roles = this.getSeedArray<CreateRoleMetadataDto>(overallMetadata?.roles);
|
|
595
|
-
//
|
|
619
|
+
// Preserve module linkage metadata so RoleMetadata.module is populated from the defining module.
|
|
596
620
|
await this.timeOperation('roles-create-if-not-exists', () => this.roleService.createRolesIfNotExists(
|
|
597
621
|
roles
|
|
598
622
|
.filter((role) => role?.name)
|
|
599
|
-
.map((role) => ({
|
|
623
|
+
.map((role) => ({
|
|
624
|
+
name: role.name,
|
|
625
|
+
moduleId: role.moduleId,
|
|
626
|
+
moduleUserKey: role.moduleUserKey ?? overallMetadata?.moduleMetadata?.name,
|
|
627
|
+
} as CreateRoleMetadataDto)),
|
|
600
628
|
), {
|
|
601
629
|
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
602
630
|
component: 'roles',
|
|
@@ -1872,6 +1900,48 @@ export class ModuleMetadataSeederService {
|
|
|
1872
1900
|
}
|
|
1873
1901
|
}
|
|
1874
1902
|
|
|
1903
|
+
private async handleSeedLocales(localesDto: CreateLocaleDto[]) {
|
|
1904
|
+
if (!localesDto || localesDto.length === 0) {
|
|
1905
|
+
this.logger.debug(`No locales found to seed`);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
for (const dto of localesDto) {
|
|
1910
|
+
if (dto.isDefault) {
|
|
1911
|
+
const existingDefaultLocales = await this.localeRepo.find({
|
|
1912
|
+
where: {
|
|
1913
|
+
isDefault: true,
|
|
1914
|
+
} as any,
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
for (const locale of existingDefaultLocales) {
|
|
1918
|
+
if (locale.locale !== dto.locale) {
|
|
1919
|
+
await this.localeRepo.save({
|
|
1920
|
+
...locale,
|
|
1921
|
+
isDefault: false,
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
const existingLocale = await this.localeRepo.findOne({
|
|
1928
|
+
where: {
|
|
1929
|
+
locale: dto.locale,
|
|
1930
|
+
} as any,
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
await this.localeRepo.save(
|
|
1934
|
+
this.localeRepo.create({
|
|
1935
|
+
...existingLocale,
|
|
1936
|
+
...dto,
|
|
1937
|
+
})
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const locales = await this.localeRepo.find();
|
|
1942
|
+
this.solidRegistry.registerlocales(locales);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1875
1945
|
private validateSavedFilterQueryJsonWrapper(dto: CreateSavedFiltersDto): void {
|
|
1876
1946
|
const filterName = dto?.name ?? '<unnamed>';
|
|
1877
1947
|
const filterQueryJson = dto?.filterQueryJson;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
1. Do i need to create a storeStreams method for aws service too?
|
|
2
|
+
- Handle later
|
|
3
|
+
2. queues handling -> if queues is enabled by default, i.e triggerExport(exportTransactionEntity.id).
|
|
4
|
+
- startExport should either return the data or return the transaction id
|
|
5
|
+
3. How to handle scenarios wherein, nested related exist.(do i need to only get the userkey)
|
|
6
|
+
- show the userKey
|
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,6 +8,7 @@ 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
|
|
|
@@ -65,13 +66,16 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
65
66
|
for (let id = 0; id < roles.length; id++) {
|
|
66
67
|
try {
|
|
67
68
|
const roleObj = roles[id];
|
|
69
|
+
const module = await this.resolveRoleModule(roleObj);
|
|
68
70
|
// this.logger.log(`Resolving role: ${JSON.stringify(roleObj)}`);
|
|
69
71
|
|
|
70
72
|
const existingRole = await this.repo.findOne({
|
|
71
73
|
where: {
|
|
72
74
|
name: roleObj.name,
|
|
73
75
|
},
|
|
74
|
-
relations: {
|
|
76
|
+
relations: {
|
|
77
|
+
module: true,
|
|
78
|
+
},
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
// Create only if not existing already.
|
|
@@ -87,9 +91,13 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
// const role = this.repo.create({ ...roleObj, permissions });
|
|
90
|
-
const role = this.repo.create({ ...roleObj });
|
|
94
|
+
const role = this.repo.create({ ...roleObj, ...(module ? { module } : {}) });
|
|
91
95
|
await this.repo.save(role);
|
|
92
96
|
} else {
|
|
97
|
+
if (module && existingRole.module?.id !== module.id) {
|
|
98
|
+
existingRole.module = module;
|
|
99
|
+
await this.repo.save(existingRole);
|
|
100
|
+
}
|
|
93
101
|
/*
|
|
94
102
|
this.logger.debug(`Role ${roleObj.name} already exists`);
|
|
95
103
|
const existingPermissions = existingRole.permissions.map(permission => permission.name);
|
|
@@ -112,6 +120,22 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
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
|
+
|
|
115
139
|
async addAllPermissionsToRole(roleName: string): Promise<RoleMetadata> {
|
|
116
140
|
return await this._addPermissionsToRole(roleName, null);
|
|
117
141
|
}
|
|
@@ -151,15 +151,11 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
|
|
|
151
151
|
return { records: [], defaultEntityLocaleId: null };
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (entityRecord.localeName === defaultLocale?.locale) {
|
|
158
|
-
defaultEntityLocaleId = entityRecord.id;
|
|
159
|
-
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}`);
|
|
160
157
|
} else {
|
|
161
|
-
|
|
162
|
-
this.logger.debug(`Editing non-default locale record. DefaultEntityLocaleId: ${defaultEntityLocaleId}`);
|
|
158
|
+
this.logger.debug(`Editing translation root record with id ${defaultEntityLocaleId}`);
|
|
163
159
|
}
|
|
164
160
|
|
|
165
161
|
const records = await currentEntityRepository.find({
|
|
@@ -84,24 +84,44 @@ export class AuditSubscriber implements EntitySubscriberInterface {
|
|
|
84
84
|
const batch = this.perTxn.get(event.queryRunner) ?? [];
|
|
85
85
|
this.perTxn.delete(event.queryRunner);
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
this.logger.error(
|
|
96
|
-
`Failed to publish audit event for ${batch[i].modelName}#${batch[i].entityId}`,
|
|
97
|
-
result.reason,
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
87
|
+
if (batch.length === 0) return;
|
|
88
|
+
|
|
89
|
+
if (this.shouldDeferAuditPublish()) {
|
|
90
|
+
setImmediate(() => void this.publishAuditBatch(batch));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await this.publishAuditBatch(batch);
|
|
101
95
|
}
|
|
102
96
|
|
|
103
97
|
afterTransactionRollback(event: { queryRunner: any }) {
|
|
104
98
|
// Drop buffered payloads; the write never happened.
|
|
105
99
|
this.perTxn.delete(event.queryRunner);
|
|
106
100
|
}
|
|
101
|
+
|
|
102
|
+
private shouldDeferAuditPublish(): boolean {
|
|
103
|
+
const driver = (process.env.DEFAULT_DATABASE_DRIVER ?? '').toLowerCase();
|
|
104
|
+
const poolMax = Number(process.env.DEFAULT_DATABASE_POOL_MAX ?? 20);
|
|
105
|
+
return driver === 'pglite' && Number.isFinite(poolMax) && poolMax <= 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async publishAuditBatch(batch: AuditQueuePayload[]) {
|
|
109
|
+
try {
|
|
110
|
+
// allSettled: publish in parallel; a single failure does not block the rest.
|
|
111
|
+
const results = await Promise.allSettled(
|
|
112
|
+
batch.map(payload => this.publisherFactory.publish({ payload }, 'ChatterQueuePublisher'))
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
results.forEach((result, i) => {
|
|
116
|
+
if (result.status === 'rejected') {
|
|
117
|
+
this.logger.error(
|
|
118
|
+
`Failed to publish audit event for ${batch[i].modelName}#${batch[i].entityId}`,
|
|
119
|
+
result.reason,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.logger.error('Failed to publish audit batch', error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
107
127
|
}
|
|
@@ -175,6 +175,7 @@ export class ComputedEntityFieldSubscriber implements EntitySubscriberInterface
|
|
|
175
175
|
const base: TypeOrmEventContext = {
|
|
176
176
|
metadataName: event.metadata?.name,
|
|
177
177
|
eventType: eventType,
|
|
178
|
+
manager: event.manager,
|
|
178
179
|
};
|
|
179
180
|
if ("entityId" in event && event.entityId) {
|
|
180
181
|
base.entityId = event.entityId;
|
|
@@ -38,6 +38,9 @@ export class CreatedByUpdatedBySubscriber implements EntitySubscriberInterface {
|
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// NOTE: If re-enabling loadUser below, query via `event.queryRunner.manager`, NOT
|
|
42
|
+
// `this.defaultDataSource.getRepository(...)`. A second connection opened mid-transaction
|
|
43
|
+
// deadlocks single-threaded engines (PGlite).
|
|
41
44
|
// const loadedUser = await this.loadUser(activeUserOrUndefined as unknown as ActiveUserData);
|
|
42
45
|
// if (isInsert) {
|
|
43
46
|
// event.entity.createdBy = loadedUser?.id;
|
|
@@ -21,6 +21,9 @@ export class FieldMetadataSubscriber implements EntitySubscriberInterface<FieldM
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
async afterUpdate(event: UpdateEvent<FieldMetadata>): Promise<void> {
|
|
24
|
+
// NOTE: If re-enabling the block below, use `event.queryRunner.manager` (the active
|
|
25
|
+
// transaction's connection), NOT `this.dataSource.getRepository(...)`. A second
|
|
26
|
+
// connection opened here mid-transaction deadlocks single-threaded engines (PGlite).
|
|
24
27
|
// If a field of type relation.many-to-one is marked for removal, and this field had an inverse relation created
|
|
25
28
|
// It means there is a field of type relation.one-to-many in the co-model, which needs to be removed
|
|
26
29
|
// if (event.entity && event.entity.isMarkedForRemoval && event.entity.type === 'relation' && event.entity.relationType === 'many-to-one' && event.entity.relationCreateInverse === true) {
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const test_1 = require("@playwright/test");
|
|
4
|
-
const env_1 = require("../helpers/env");
|
|
5
|
-
const baseURL = process.env.API_BASE_URL ?? "http://localhost:3000";
|
|
6
|
-
const TEST_USER_EMAIL = (0, env_1.getRequiredEnv)("TEST_USER_EMAIL");
|
|
7
|
-
const TEST_USER_PASSWORD = (0, env_1.getRequiredEnv)("TEST_USER_PASSWORD");
|
|
8
|
-
function base64UrlDecode(input) {
|
|
9
|
-
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
10
|
-
const padded = normalized.length % 4 === 0
|
|
11
|
-
? normalized
|
|
12
|
-
: normalized.padEnd(normalized.length + (4 - (normalized.length % 4)), "=");
|
|
13
|
-
return Buffer.from(padded, "base64").toString("utf-8");
|
|
14
|
-
}
|
|
15
|
-
function validateJwt(token) {
|
|
16
|
-
const parts = token.split(".");
|
|
17
|
-
if (parts.length !== 3) {
|
|
18
|
-
throw new Error("JWT must have three dot-separated parts.");
|
|
19
|
-
}
|
|
20
|
-
const headerJson = JSON.parse(base64UrlDecode(parts[0]));
|
|
21
|
-
const payloadJson = JSON.parse(base64UrlDecode(parts[1]));
|
|
22
|
-
if (!headerJson || typeof headerJson !== "object") {
|
|
23
|
-
throw new Error("JWT header must be a JSON object.");
|
|
24
|
-
}
|
|
25
|
-
if (!payloadJson || typeof payloadJson !== "object") {
|
|
26
|
-
throw new Error("JWT payload must be a JSON object.");
|
|
27
|
-
}
|
|
28
|
-
if (typeof payloadJson.exp !== "number") {
|
|
29
|
-
throw new Error("JWT payload.exp must be a number.");
|
|
30
|
-
}
|
|
31
|
-
return payloadJson;
|
|
32
|
-
}
|
|
33
|
-
(0, test_1.test)("API: authenticate succeeds with valid credentials", async () => {
|
|
34
|
-
const api = await test_1.request.newContext({
|
|
35
|
-
baseURL,
|
|
36
|
-
extraHTTPHeaders: {
|
|
37
|
-
accept: "*/*",
|
|
38
|
-
"content-type": "application/json",
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
try {
|
|
42
|
-
const res = await api.post("/api/iam/authenticate", {
|
|
43
|
-
data: {
|
|
44
|
-
email: TEST_USER_EMAIL,
|
|
45
|
-
username: "",
|
|
46
|
-
password: TEST_USER_PASSWORD,
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
(0, test_1.expect)(res.status()).toBe(200);
|
|
50
|
-
const json = await res.json();
|
|
51
|
-
(0, test_1.expect)(json.statusCode).toBe(200);
|
|
52
|
-
(0, test_1.expect)(Array.isArray(json.message)).toBe(true);
|
|
53
|
-
(0, test_1.expect)(json.message.length).toBe(0);
|
|
54
|
-
(0, test_1.expect)(json.error).toBe("");
|
|
55
|
-
const user = json.data?.user;
|
|
56
|
-
(0, test_1.expect)(user, "Expected data.user to be an object.").toBeTruthy();
|
|
57
|
-
(0, test_1.expect)(typeof user).toBe("object");
|
|
58
|
-
const email = user?.email;
|
|
59
|
-
(0, test_1.expect)(typeof email).toBe("string");
|
|
60
|
-
if (email === TEST_USER_EMAIL) {
|
|
61
|
-
(0, test_1.expect)(email).toBe(TEST_USER_EMAIL);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
(0, test_1.expect)(email.length).toBeGreaterThan(0);
|
|
65
|
-
}
|
|
66
|
-
(0, test_1.expect)(typeof user?.mobile).toBe("string");
|
|
67
|
-
(0, test_1.expect)(typeof user?.username).toBe("string");
|
|
68
|
-
(0, test_1.expect)(typeof user?.forcePasswordChange).toBe("boolean");
|
|
69
|
-
(0, test_1.expect)(typeof user?.id).toBe("number");
|
|
70
|
-
const roles = user?.roles;
|
|
71
|
-
(0, test_1.expect)(Array.isArray(roles)).toBe(true);
|
|
72
|
-
if (Array.isArray(roles)) {
|
|
73
|
-
(0, test_1.expect)(roles.every((role) => typeof role === "string")).toBe(true);
|
|
74
|
-
(0, test_1.expect)(roles).toContain("Admin");
|
|
75
|
-
}
|
|
76
|
-
const accessToken = json.data?.accessToken;
|
|
77
|
-
const refreshToken = json.data?.refreshToken;
|
|
78
|
-
(0, test_1.expect)(typeof accessToken).toBe("string");
|
|
79
|
-
(0, test_1.expect)(typeof refreshToken).toBe("string");
|
|
80
|
-
const accessPayload = validateJwt(accessToken);
|
|
81
|
-
const refreshPayload = validateJwt(refreshToken);
|
|
82
|
-
(0, test_1.expect)(typeof accessPayload.exp).toBe("number");
|
|
83
|
-
(0, test_1.expect)(typeof refreshPayload.exp).toBe("number");
|
|
84
|
-
}
|
|
85
|
-
finally {
|
|
86
|
-
await api.dispose();
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
(0, test_1.test)("API: authenticate fails with wrong password", async () => {
|
|
90
|
-
const api = await test_1.request.newContext({
|
|
91
|
-
baseURL,
|
|
92
|
-
extraHTTPHeaders: {
|
|
93
|
-
accept: "*/*",
|
|
94
|
-
"content-type": "application/json",
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
try {
|
|
98
|
-
const res = await api.post("/api/iam/authenticate", {
|
|
99
|
-
data: {
|
|
100
|
-
email: TEST_USER_EMAIL,
|
|
101
|
-
username: "",
|
|
102
|
-
password: `${TEST_USER_PASSWORD}__wrong`,
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
(0, test_1.expect)(res.status()).toBe(401);
|
|
106
|
-
const json = await res.json();
|
|
107
|
-
(0, test_1.expect)(json.statusCode).toBe(401);
|
|
108
|
-
(0, test_1.expect)(json.statusCodeMessage).toBe("Unauthorized");
|
|
109
|
-
(0, test_1.expect)(json.message).toBe("Invalid credentials");
|
|
110
|
-
(0, test_1.expect)(json.error).toBe("Invalid credentials");
|
|
111
|
-
(0, test_1.expect)(json.data?.statusCode).toBe(401);
|
|
112
|
-
(0, test_1.expect)(json.data?.error).toBe("Unauthorized");
|
|
113
|
-
(0, test_1.expect)(json.data?.message).toBe("Invalid credentials");
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
await api.dispose();
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
//# sourceMappingURL=authenticate.spec.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"authenticate.spec.js","sourceRoot":"","sources":["../../tests/api/authenticate.spec.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, request, test } from \"@playwright/test\";\nimport { getRequiredEnv } from \"../helpers/env\";\n\ntype JwtPayload = {\n exp?: number;\n [key: string]: unknown;\n};\n\nconst baseURL = process.env.API_BASE_URL ?? \"http://localhost:3000\";\nconst TEST_USER_EMAIL = getRequiredEnv(\"TEST_USER_EMAIL\");\nconst TEST_USER_PASSWORD = getRequiredEnv(\"TEST_USER_PASSWORD\");\n\nfunction base64UrlDecode(input: string): string {\n const normalized = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded =\n normalized.length % 4 === 0\n ? normalized\n : normalized.padEnd(\n normalized.length + (4 - (normalized.length % 4)),\n \"=\"\n );\n return Buffer.from(padded, \"base64\").toString(\"utf-8\");\n}\n\nfunction validateJwt(token: string): JwtPayload {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n throw new Error(\"JWT must have three dot-separated parts.\");\n }\n\n const headerJson = JSON.parse(base64UrlDecode(parts[0]));\n const payloadJson = JSON.parse(base64UrlDecode(parts[1]));\n\n if (!headerJson || typeof headerJson !== \"object\") {\n throw new Error(\"JWT header must be a JSON object.\");\n }\n\n if (!payloadJson || typeof payloadJson !== \"object\") {\n throw new Error(\"JWT payload must be a JSON object.\");\n }\n\n if (typeof payloadJson.exp !== \"number\") {\n throw new Error(\"JWT payload.exp must be a number.\");\n }\n\n return payloadJson as JwtPayload;\n}\n\ntest(\"API: authenticate succeeds with valid credentials\", async () => {\n const api = await request.newContext({\n baseURL,\n extraHTTPHeaders: {\n accept: \"*/*\",\n \"content-type\": \"application/json\",\n },\n });\n\n try {\n const res = await api.post(\"/api/iam/authenticate\", {\n data: {\n email: TEST_USER_EMAIL,\n username: \"\",\n password: TEST_USER_PASSWORD,\n },\n });\n\n expect(res.status()).toBe(200);\n const json = await res.json();\n\n expect(json.statusCode).toBe(200);\n expect(Array.isArray(json.message)).toBe(true);\n expect(json.message.length).toBe(0);\n expect(json.error).toBe(\"\");\n\n const user = json.data?.user as Record<string, unknown> | undefined;\n expect(user, \"Expected data.user to be an object.\").toBeTruthy();\n expect(typeof user).toBe(\"object\");\n\n const email = user?.email;\n expect(typeof email).toBe(\"string\");\n if (email === TEST_USER_EMAIL) {\n expect(email).toBe(TEST_USER_EMAIL);\n } else {\n // If your API returns a fixed system email, replace this with an exact match.\n expect((email as string).length).toBeGreaterThan(0);\n }\n\n expect(typeof user?.mobile).toBe(\"string\");\n expect(typeof user?.username).toBe(\"string\");\n expect(typeof user?.forcePasswordChange).toBe(\"boolean\");\n expect(typeof user?.id).toBe(\"number\");\n\n const roles = user?.roles;\n expect(Array.isArray(roles)).toBe(true);\n if (Array.isArray(roles)) {\n expect(roles.every((role) => typeof role === \"string\")).toBe(true);\n expect(roles).toContain(\"Admin\");\n }\n\n const accessToken = json.data?.accessToken;\n const refreshToken = json.data?.refreshToken;\n expect(typeof accessToken).toBe(\"string\");\n expect(typeof refreshToken).toBe(\"string\");\n\n const accessPayload = validateJwt(accessToken as string);\n const refreshPayload = validateJwt(refreshToken as string);\n\n expect(typeof accessPayload.exp).toBe(\"number\");\n expect(typeof refreshPayload.exp).toBe(\"number\");\n } finally {\n await api.dispose();\n }\n});\n\ntest(\"API: authenticate fails with wrong password\", async () => {\n const api = await request.newContext({\n baseURL,\n extraHTTPHeaders: {\n accept: \"*/*\",\n \"content-type\": \"application/json\",\n },\n });\n\n try {\n const res = await api.post(\"/api/iam/authenticate\", {\n data: {\n email: TEST_USER_EMAIL,\n username: \"\",\n password: `${TEST_USER_PASSWORD}__wrong`,\n },\n });\n\n expect(res.status()).toBe(401);\n const json = await res.json();\n\n expect(json.statusCode).toBe(401);\n expect(json.statusCodeMessage).toBe(\"Unauthorized\");\n expect(json.message).toBe(\"Invalid credentials\");\n expect(json.error).toBe(\"Invalid credentials\");\n expect(json.data?.statusCode).toBe(401);\n expect(json.data?.error).toBe(\"Unauthorized\");\n expect(json.data?.message).toBe(\"Invalid credentials\");\n } finally {\n await api.dispose();\n }\n});\n"]}
|