@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.
Files changed (80) hide show
  1. package/.claude/settings.local.json +15 -0
  2. package/CLAUDE.md +26 -0
  3. package/dist/controllers/service.controller.d.ts +1 -0
  4. package/dist/controllers/service.controller.d.ts.map +1 -1
  5. package/dist/controllers/service.controller.js +2 -2
  6. package/dist/controllers/service.controller.js.map +1 -1
  7. package/dist/helpers/field-crud-managers/IntFieldCrudManager.d.ts.map +1 -1
  8. package/dist/helpers/field-crud-managers/IntFieldCrudManager.js +4 -1
  9. package/dist/helpers/field-crud-managers/IntFieldCrudManager.js.map +1 -1
  10. package/dist/helpers/solid-registry.d.ts +2 -1
  11. package/dist/helpers/solid-registry.d.ts.map +1 -1
  12. package/dist/helpers/solid-registry.js.map +1 -1
  13. package/dist/seeders/module-metadata-seeder.service.d.ts +5 -1
  14. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  15. package/dist/seeders/module-metadata-seeder.service.js +61 -3
  16. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  17. package/dist/seeders/seed-data/solid-core-metadata.json +7 -0
  18. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.d.ts.map +1 -1
  19. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js +6 -5
  20. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js.map +1 -1
  21. package/dist/services/locale.service.d.ts +14 -0
  22. package/dist/services/locale.service.d.ts.map +1 -1
  23. package/dist/services/locale.service.js +83 -0
  24. package/dist/services/locale.service.js.map +1 -1
  25. package/dist/services/role-metadata.service.d.ts +1 -0
  26. package/dist/services/role-metadata.service.d.ts.map +1 -1
  27. package/dist/services/role-metadata.service.js +23 -2
  28. package/dist/services/role-metadata.service.js.map +1 -1
  29. package/dist/services/view-metadata.service.d.ts.map +1 -1
  30. package/dist/services/view-metadata.service.js +4 -7
  31. package/dist/services/view-metadata.service.js.map +1 -1
  32. package/dist/subscribers/audit.subscriber.d.ts +2 -0
  33. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  34. package/dist/subscribers/audit.subscriber.js +25 -6
  35. package/dist/subscribers/audit.subscriber.js.map +1 -1
  36. package/dist/subscribers/computed-entity-field.subscriber.d.ts.map +1 -1
  37. package/dist/subscribers/computed-entity-field.subscriber.js +1 -0
  38. package/dist/subscribers/computed-entity-field.subscriber.js.map +1 -1
  39. package/dist/subscribers/created-by-updated-by.subscriber.d.ts.map +1 -1
  40. package/dist/subscribers/created-by-updated-by.subscriber.js.map +1 -1
  41. package/dist/subscribers/field-metadata.subscriber.d.ts.map +1 -1
  42. package/dist/subscribers/field-metadata.subscriber.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/controllers/service.controller.ts +3 -2
  45. package/src/helpers/field-crud-managers/IntFieldCrudManager.ts +4 -1
  46. package/src/helpers/solid-registry.ts +7 -1
  47. package/src/seeders/module-metadata-seeder.service.ts +72 -2
  48. package/src/seeders/seed-data/solid-core-metadata.json +7 -0
  49. package/src/services/1.js +6 -0
  50. package/src/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.ts +10 -5
  51. package/src/services/locale.service.ts +111 -3
  52. package/src/services/role-metadata.service.ts +26 -2
  53. package/src/services/view-metadata.service.ts +4 -8
  54. package/src/subscribers/audit.subscriber.ts +34 -14
  55. package/src/subscribers/computed-entity-field.subscriber.ts +1 -0
  56. package/src/subscribers/created-by-updated-by.subscriber.ts +3 -0
  57. package/src/subscribers/field-metadata.subscriber.ts +3 -0
  58. package/dist-tests/api/authenticate.spec.js +0 -119
  59. package/dist-tests/api/authenticate.spec.js.map +0 -1
  60. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
  61. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
  62. package/dist-tests/api/ping.spec.js +0 -21
  63. package/dist-tests/api/ping.spec.js.map +0 -1
  64. package/dist-tests/helpers/auth.js +0 -41
  65. package/dist-tests/helpers/auth.js.map +0 -1
  66. package/dist-tests/helpers/env.js +0 -11
  67. package/dist-tests/helpers/env.js.map +0 -1
  68. package/docs/agent-hub-grooming.md +0 -301
  69. package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
  70. package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
  71. package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
  72. package/docs/datasource-introspection-ddl-analysis.md +0 -326
  73. package/docs/datasource-introspection-implementation-plan.md +0 -306
  74. package/docs/grouping-enhancements.md +0 -89
  75. package/docs/java-spring/README.md +0 -3
  76. package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
  77. package/docs/module-package-import-handoff.md +0 -691
  78. package/docs/seed-changes.md +0 -65
  79. package/docs/test-data-workflow.md +0 -200
  80. 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
- // While creating roles we are only passing the role name to be used.
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) => ({ name: role.name } as any)),
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;
@@ -5244,6 +5244,13 @@
5244
5244
  }
5245
5245
  ]
5246
5246
  },
5247
+ "locales": [
5248
+ {
5249
+ "locale": "en",
5250
+ "displayName": "English",
5251
+ "isDefault": true
5252
+ }
5253
+ ],
5247
5254
  "permissions": [
5248
5255
  "mcp:invoke",
5249
5256
  "agent:invoke",
@@ -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
@@ -45,7 +45,12 @@ export class AlphaNumExternalIdComputationProvider<T extends CommonEntity> imple
45
45
  }
46
46
  }
47
47
 
48
- const uniqueCode = await this.generateUniqueExternalId(resolvedPrefix, codeLength, computedFieldMetadata.fieldName, entityName);
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 this.entityManager.count(entityName as any,
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
- async loadLocales() {
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 defaultLocale = await this.entityManager.getRepository(Locale).findOne({ where: { isDefault: true } });
155
-
156
- let defaultEntityLocaleId: string;
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
- defaultEntityLocaleId = entityRecord.defaultEntityLocaleId;
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
- // Now outside the DB transaction — safe to publish to the queue.
88
- // allSettled: publish in parallel; a single failure does not block the rest.
89
- const results = await Promise.allSettled(
90
- batch.map(payload => this.publisherFactory.publish({ payload }, 'ChatterQueuePublisher'))
91
- );
92
-
93
- results.forEach((result, i) => {
94
- if (result.status === 'rejected') {
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"]}