@flusys/nestjs-localization 6.0.1 → 6.1.0

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.
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _testing = require("@nestjs/testing");
6
+ const _modules = require("@flusys/nestjs-shared/modules");
7
+ const _repositorymock = require("@test-utils/mocks/repository.mock");
8
+ const _languageservice = require("./language.service");
9
+ const _localizationdatasourceprovider = require("./localization-datasource.provider");
10
+ function buildLanguage(overrides = {}) {
11
+ return {
12
+ id: 'language-uuid-1',
13
+ code: 'en',
14
+ name: 'English',
15
+ nativeName: 'English',
16
+ direction: 'ltr',
17
+ isActive: true,
18
+ isDefault: false,
19
+ serial: 1,
20
+ isReadonly: false,
21
+ createdAt: new Date(),
22
+ updatedAt: new Date(),
23
+ deletedAt: null,
24
+ createdById: null,
25
+ updatedById: null,
26
+ deletedById: null,
27
+ ...overrides
28
+ };
29
+ }
30
+ describe('LanguageService', ()=>{
31
+ let service;
32
+ let mockRepo;
33
+ beforeEach(async ()=>{
34
+ mockRepo = (0, _repositorymock.createMockRepository)();
35
+ const mockDataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
36
+ const module = await _testing.Test.createTestingModule({
37
+ providers: [
38
+ _languageservice.LanguageService,
39
+ {
40
+ provide: 'CACHE_INSTANCE',
41
+ useValue: {}
42
+ },
43
+ {
44
+ provide: _modules.UtilsService,
45
+ useValue: {}
46
+ },
47
+ {
48
+ provide: _localizationdatasourceprovider.LocalizationDataSourceProvider,
49
+ useValue: mockDataSourceProvider
50
+ }
51
+ ]
52
+ }).compile();
53
+ service = await module.resolve(_languageservice.LanguageService);
54
+ });
55
+ describe('getDefaultLanguage', ()=>{
56
+ it('returns the language flagged as default', async ()=>{
57
+ const defaultLanguage = buildLanguage({
58
+ id: 'default-lang',
59
+ isDefault: true,
60
+ code: 'en'
61
+ });
62
+ mockRepo.findOne.mockResolvedValue(defaultLanguage);
63
+ const result = await service.getDefaultLanguage();
64
+ expect(mockRepo.findOne).toHaveBeenCalledWith({
65
+ where: {
66
+ isDefault: true,
67
+ deletedAt: expect.anything()
68
+ }
69
+ });
70
+ expect(result).toEqual(defaultLanguage);
71
+ });
72
+ it('returns null when no language is marked default', async ()=>{
73
+ mockRepo.findOne.mockResolvedValue(null);
74
+ const result = await service.getDefaultLanguage();
75
+ expect(result).toBeNull();
76
+ });
77
+ });
78
+ describe('getActiveLanguages', ()=>{
79
+ it('returns active, non-deleted languages ordered by serial then name', async ()=>{
80
+ const languages = [
81
+ buildLanguage({
82
+ code: 'en',
83
+ serial: 1
84
+ }),
85
+ buildLanguage({
86
+ code: 'bn',
87
+ serial: 2
88
+ })
89
+ ];
90
+ mockRepo.find.mockResolvedValue(languages);
91
+ const result = await service.getActiveLanguages();
92
+ expect(mockRepo.find).toHaveBeenCalledWith({
93
+ where: {
94
+ isActive: true,
95
+ deletedAt: expect.anything()
96
+ },
97
+ order: {
98
+ serial: 'ASC',
99
+ name: 'ASC'
100
+ }
101
+ });
102
+ expect(result).toHaveLength(2);
103
+ });
104
+ it('returns an empty array when no languages are active', async ()=>{
105
+ mockRepo.find.mockResolvedValue([]);
106
+ const result = await service.getActiveLanguages();
107
+ expect(result).toEqual([]);
108
+ });
109
+ });
110
+ describe('setAsDefault', ()=>{
111
+ it('unsets the previous default and sets the requested language as default', async ()=>{
112
+ const newDefault = buildLanguage({
113
+ id: 'lang-2',
114
+ isDefault: true,
115
+ code: 'bn'
116
+ });
117
+ mockRepo.findOne.mockResolvedValue(newDefault);
118
+ const result = await service.setAsDefault('lang-2');
119
+ expect(mockRepo.update).toHaveBeenNthCalledWith(1, {
120
+ isDefault: true
121
+ }, {
122
+ isDefault: false
123
+ });
124
+ expect(mockRepo.update).toHaveBeenNthCalledWith(2, {
125
+ id: 'lang-2'
126
+ }, {
127
+ isDefault: true
128
+ });
129
+ expect(mockRepo.findOne).toHaveBeenCalledWith({
130
+ where: {
131
+ id: 'lang-2'
132
+ }
133
+ });
134
+ expect(result).toEqual(newDefault);
135
+ });
136
+ it('returns null when the id does not correspond to any language after the update', async ()=>{
137
+ mockRepo.findOne.mockResolvedValue(null);
138
+ const result = await service.setAsDefault('non-existent-id');
139
+ expect(result).toBeNull();
140
+ });
141
+ it('still unsets other defaults first even when the target id is unknown (no exception thrown)', async ()=>{
142
+ mockRepo.findOne.mockResolvedValue(null);
143
+ await service.setAsDefault('unknown-id');
144
+ expect(mockRepo.update).toHaveBeenCalledWith({
145
+ isDefault: true
146
+ }, {
147
+ isDefault: false
148
+ });
149
+ });
150
+ });
151
+ });
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _localizationconfigservice = require("./localization-config.service");
6
+ function buildOptions(overrides = {}) {
7
+ return {
8
+ bootstrapAppConfig: undefined,
9
+ config: undefined,
10
+ ...overrides
11
+ };
12
+ }
13
+ describe('LocalizationConfigService', ()=>{
14
+ describe('isCompanyFeatureEnabled', ()=>{
15
+ it('always returns false — localization module has no company scoping', ()=>{
16
+ const service = new _localizationconfigservice.LocalizationConfigService(buildOptions());
17
+ expect(service.isCompanyFeatureEnabled()).toBe(false);
18
+ });
19
+ it('returns false even when bootstrapAppConfig enables the company feature elsewhere', ()=>{
20
+ const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
21
+ bootstrapAppConfig: {
22
+ databaseMode: 'single',
23
+ enableCompanyFeature: true
24
+ }
25
+ }));
26
+ expect(service.isCompanyFeatureEnabled()).toBe(false);
27
+ });
28
+ });
29
+ describe('getDatabaseMode', ()=>{
30
+ it('returns the configured database mode', ()=>{
31
+ const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
32
+ bootstrapAppConfig: {
33
+ databaseMode: 'multi-tenant'
34
+ }
35
+ }));
36
+ expect(service.getDatabaseMode()).toBe('multi-tenant');
37
+ });
38
+ it('defaults to "single" when bootstrapAppConfig is missing', ()=>{
39
+ const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
40
+ bootstrapAppConfig: undefined
41
+ }));
42
+ expect(service.getDatabaseMode()).toBe('single');
43
+ });
44
+ it('defaults to "single" when databaseMode is not set on bootstrapAppConfig', ()=>{
45
+ const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
46
+ bootstrapAppConfig: {}
47
+ }));
48
+ expect(service.getDatabaseMode()).toBe('single');
49
+ });
50
+ });
51
+ describe('isMultiTenant', ()=>{
52
+ it('returns true when database mode is multi-tenant', ()=>{
53
+ const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
54
+ bootstrapAppConfig: {
55
+ databaseMode: 'multi-tenant'
56
+ }
57
+ }));
58
+ expect(service.isMultiTenant()).toBe(true);
59
+ });
60
+ it('returns false when database mode is single', ()=>{
61
+ const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
62
+ bootstrapAppConfig: {
63
+ databaseMode: 'single'
64
+ }
65
+ }));
66
+ expect(service.isMultiTenant()).toBe(false);
67
+ });
68
+ });
69
+ describe('getOptions', ()=>{
70
+ it('returns the exact options object it was constructed with', ()=>{
71
+ const options = buildOptions({
72
+ config: {
73
+ defaultLanguageCode: 'en'
74
+ }
75
+ });
76
+ const service = new _localizationconfigservice.LocalizationConfigService(options);
77
+ expect(service.getOptions()).toBe(options);
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _localizationdatasourceprovider = require("./localization-datasource.provider");
6
+ const _entities = require("../entities");
7
+ function buildConfigService(overrides = {}) {
8
+ const options = {
9
+ bootstrapAppConfig: {
10
+ databaseMode: 'single'
11
+ },
12
+ config: {
13
+ defaultDatabaseConfig: {
14
+ type: 'mysql',
15
+ host: 'localhost',
16
+ port: 3306,
17
+ username: 'root',
18
+ password: 'password',
19
+ database: 'flusys_test'
20
+ },
21
+ tenantDefaultDatabaseConfig: undefined,
22
+ tenants: []
23
+ },
24
+ ...overrides
25
+ };
26
+ return {
27
+ getOptions: jest.fn().mockReturnValue(options)
28
+ };
29
+ }
30
+ describe('LocalizationDataSourceProvider', ()=>{
31
+ afterEach(()=>{
32
+ // Reset the isolated static caches this provider owns so tests don't leak state between runs.
33
+ _localizationdatasourceprovider.LocalizationDataSourceProvider.tenantConnections = new Map();
34
+ _localizationdatasourceprovider.LocalizationDataSourceProvider.singleDataSource = null;
35
+ _localizationdatasourceprovider.LocalizationDataSourceProvider.tenantsRegistry = new Map();
36
+ _localizationdatasourceprovider.LocalizationDataSourceProvider.initialized = false;
37
+ _localizationdatasourceprovider.LocalizationDataSourceProvider.connectionLocks = new Map();
38
+ _localizationdatasourceprovider.LocalizationDataSourceProvider.singleConnectionLock = null;
39
+ });
40
+ it('constructs and forwards bootstrap/database config to the base class', ()=>{
41
+ const configService = buildConfigService();
42
+ const provider = new _localizationdatasourceprovider.LocalizationDataSourceProvider(configService, undefined);
43
+ expect(configService.getOptions).toHaveBeenCalled();
44
+ expect(provider.getDatabaseMode()).toBe('single');
45
+ expect(provider.isMultiTenant()).toBe(false);
46
+ });
47
+ it('reflects multi-tenant mode from bootstrapAppConfig', ()=>{
48
+ const configService = buildConfigService({
49
+ bootstrapAppConfig: {
50
+ databaseMode: 'multi-tenant'
51
+ }
52
+ });
53
+ const provider = new _localizationdatasourceprovider.LocalizationDataSourceProvider(configService, undefined);
54
+ expect(provider.isMultiTenant()).toBe(true);
55
+ });
56
+ it('owns isolated static caches, separate from the base MultiTenantDataSourceService', ()=>{
57
+ // Each DataSource Provider must override BOTH static cache properties and the
58
+ // methods that read them — otherwise modules would share connections (see FLUSYS_NEST/CLAUDE.md).
59
+ expect(_localizationdatasourceprovider.LocalizationDataSourceProvider.tenantConnections).not.toBe(Object.getPrototypeOf(_localizationdatasourceprovider.LocalizationDataSourceProvider).tenantConnections);
60
+ });
61
+ it('createDataSourceFromConfig builds a DataSource using all localization entities', async ()=>{
62
+ const configService = buildConfigService();
63
+ const provider = new _localizationdatasourceprovider.LocalizationDataSourceProvider(configService, undefined);
64
+ const fakeDataSource = {
65
+ isInitialized: true
66
+ };
67
+ const superSpy = jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(provider)), 'createDataSourceFromConfig').mockResolvedValue(fakeDataSource);
68
+ const config = {
69
+ type: 'mysql',
70
+ host: 'localhost',
71
+ port: 3306,
72
+ username: 'root',
73
+ password: 'password',
74
+ database: 'flusys_test'
75
+ };
76
+ const result = await provider.createDataSourceFromConfig(config);
77
+ expect(superSpy).toHaveBeenCalledWith(config, [
78
+ _entities.Language,
79
+ _entities.TranslationKey,
80
+ _entities.Translation
81
+ ]);
82
+ expect(result).toBe(fakeDataSource);
83
+ superSpy.mockRestore();
84
+ });
85
+ });
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _testing = require("@nestjs/testing");
6
+ const _modules = require("@flusys/nestjs-shared/modules");
7
+ const _common = require("@nestjs/common");
8
+ const _repositorymock = require("@test-utils/mocks/repository.mock");
9
+ const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
10
+ const _translationkeyservice = require("./translation-key.service");
11
+ const _localizationdatasourceprovider = require("./localization-datasource.provider");
12
+ function buildTranslationKey(overrides = {}) {
13
+ return {
14
+ id: 'tkey-uuid-1',
15
+ module: 'auth',
16
+ key: 'auth.login.success',
17
+ defaultMessage: 'Login successful',
18
+ description: null,
19
+ variables: null,
20
+ isActive: true,
21
+ isReadonly: false,
22
+ serial: 1,
23
+ createdAt: new Date(),
24
+ updatedAt: new Date(),
25
+ deletedAt: null,
26
+ createdById: null,
27
+ updatedById: null,
28
+ deletedById: null,
29
+ ...overrides
30
+ };
31
+ }
32
+ describe('TranslationKeyService', ()=>{
33
+ let service;
34
+ let mockRepo;
35
+ let qb;
36
+ let queryRunner;
37
+ beforeEach(async ()=>{
38
+ mockRepo = (0, _repositorymock.createMockRepository)();
39
+ qb = mockRepo.createQueryBuilder();
40
+ const mockDataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
41
+ // executeInTransaction()/delete() always grab a QueryRunner off repository.manager.connection
42
+ // before running the before*Operation hooks, so every test (success or rejection) needs one.
43
+ queryRunner = {
44
+ connect: jest.fn(),
45
+ startTransaction: jest.fn(),
46
+ commitTransaction: jest.fn(),
47
+ rollbackTransaction: jest.fn(),
48
+ release: jest.fn(),
49
+ manager: {
50
+ save: jest.fn(),
51
+ softDelete: jest.fn(),
52
+ update: jest.fn(),
53
+ delete: jest.fn(),
54
+ restore: jest.fn()
55
+ }
56
+ };
57
+ mockRepo.manager.connection.createQueryRunner.mockReturnValue(queryRunner);
58
+ const mockUtilsService = {
59
+ clearCache: jest.fn(),
60
+ getCacheKey: jest.fn().mockReturnValue('cache-key'),
61
+ trackCacheKey: jest.fn()
62
+ };
63
+ const module = await _testing.Test.createTestingModule({
64
+ providers: [
65
+ _translationkeyservice.TranslationKeyService,
66
+ {
67
+ provide: 'CACHE_INSTANCE',
68
+ useValue: {
69
+ get: jest.fn(),
70
+ set: jest.fn()
71
+ }
72
+ },
73
+ {
74
+ provide: _modules.UtilsService,
75
+ useValue: mockUtilsService
76
+ },
77
+ {
78
+ provide: _localizationdatasourceprovider.LocalizationDataSourceProvider,
79
+ useValue: mockDataSourceProvider
80
+ }
81
+ ]
82
+ }).compile();
83
+ service = await module.resolve(_translationkeyservice.TranslationKeyService);
84
+ });
85
+ describe('findByModule', ()=>{
86
+ it('returns active, non-deleted keys for the module ordered by serial then key', async ()=>{
87
+ const keys = [
88
+ buildTranslationKey({
89
+ key: 'auth.login.success',
90
+ serial: 1
91
+ })
92
+ ];
93
+ mockRepo.find.mockResolvedValue(keys);
94
+ const result = await service.findByModule('auth');
95
+ expect(mockRepo.find).toHaveBeenCalledWith({
96
+ where: {
97
+ module: 'auth',
98
+ isActive: true,
99
+ deletedAt: expect.anything()
100
+ },
101
+ order: {
102
+ serial: 'ASC',
103
+ key: 'ASC'
104
+ }
105
+ });
106
+ expect(result).toEqual(keys);
107
+ });
108
+ it('returns an empty array for an unknown module', async ()=>{
109
+ mockRepo.find.mockResolvedValue([]);
110
+ const result = await service.findByModule('does-not-exist');
111
+ expect(result).toEqual([]);
112
+ });
113
+ });
114
+ describe('getModules', ()=>{
115
+ it('returns distinct module names', async ()=>{
116
+ // `getRawMany` isn't part of the shared MockQueryBuilder — attach it for this raw-query path.
117
+ const getRawMany = jest.fn().mockResolvedValue([
118
+ {
119
+ module: 'auth'
120
+ },
121
+ {
122
+ module: 'shared'
123
+ }
124
+ ]);
125
+ qb.getRawMany = getRawMany;
126
+ const result = await service.getModules();
127
+ expect(result).toEqual([
128
+ 'auth',
129
+ 'shared'
130
+ ]);
131
+ });
132
+ it('returns an empty array when there are no translation keys yet', async ()=>{
133
+ const getRawMany = jest.fn().mockResolvedValue([]);
134
+ qb.getRawMany = getRawMany;
135
+ const result = await service.getModules();
136
+ expect(result).toEqual([]);
137
+ });
138
+ });
139
+ describe('beforeInsertOperation (via insert) — duplicate key validation', ()=>{
140
+ it('allows insert when no key with the same module+key exists', async ()=>{
141
+ qb.getOne.mockResolvedValue(null);
142
+ const created = buildTranslationKey();
143
+ mockRepo.create.mockReturnValue({});
144
+ queryRunner.manager.save.mockResolvedValue(created);
145
+ await expect(service.insert({
146
+ module: 'auth',
147
+ key: 'auth.login.success',
148
+ defaultMessage: 'Login successful'
149
+ }, (0, _loggedusermock.buildMockUser)())).resolves.toBeDefined();
150
+ });
151
+ it('rejects with BadRequestException + messageKey when the module+key combination already exists', async ()=>{
152
+ qb.getOne.mockResolvedValue(buildTranslationKey());
153
+ await expect(service.insert({
154
+ module: 'auth',
155
+ key: 'auth.login.success',
156
+ defaultMessage: 'Login successful'
157
+ }, (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
158
+ response: expect.objectContaining({
159
+ messageKey: 'translation.key.duplicate.key.in.module'
160
+ })
161
+ });
162
+ });
163
+ it('rejects with BadRequestException for duplicate module+key in insertMany', async ()=>{
164
+ qb.getOne.mockResolvedValueOnce(null).mockResolvedValueOnce(buildTranslationKey({
165
+ key: 'auth.logout.success'
166
+ }));
167
+ await expect(service.insertMany([
168
+ {
169
+ module: 'auth',
170
+ key: 'auth.login.success',
171
+ defaultMessage: 'A'
172
+ },
173
+ {
174
+ module: 'auth',
175
+ key: 'auth.logout.success',
176
+ defaultMessage: 'B'
177
+ }
178
+ ], (0, _loggedusermock.buildMockUser)())).rejects.toBeInstanceOf(_common.BadRequestException);
179
+ });
180
+ });
181
+ describe('beforeUpdateOperation (via update)', ()=>{
182
+ it('rejects with ForbiddenException when modifying key/module of a readonly translation key', async ()=>{
183
+ const existing = buildTranslationKey({
184
+ id: 'tkey-1',
185
+ isReadonly: true,
186
+ key: 'system.ready'
187
+ });
188
+ mockRepo.find.mockResolvedValue([
189
+ existing
190
+ ]);
191
+ await expect(service.update({
192
+ id: 'tkey-1',
193
+ key: 'system.changed'
194
+ }, (0, _loggedusermock.buildMockUser)())).rejects.toBeInstanceOf(_common.ForbiddenException);
195
+ await expect(service.update({
196
+ id: 'tkey-1',
197
+ key: 'system.changed'
198
+ }, (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
199
+ response: expect.objectContaining({
200
+ messageKey: 'translation.key.readonly.update.forbidden'
201
+ })
202
+ });
203
+ });
204
+ it('allows updating non key/module fields of a readonly translation key', async ()=>{
205
+ const existing = buildTranslationKey({
206
+ id: 'tkey-1',
207
+ isReadonly: true,
208
+ key: 'system.ready'
209
+ });
210
+ mockRepo.find.mockResolvedValue([
211
+ existing
212
+ ]);
213
+ mockRepo.findOne.mockResolvedValue(existing);
214
+ queryRunner.manager.save.mockResolvedValue(existing);
215
+ await expect(service.update({
216
+ id: 'tkey-1',
217
+ defaultMessage: 'Updated readonly message'
218
+ }, (0, _loggedusermock.buildMockUser)())).resolves.toBeDefined();
219
+ });
220
+ it('rejects with BadRequestException when the new key/module already exists on another row', async ()=>{
221
+ const existing = buildTranslationKey({
222
+ id: 'tkey-1',
223
+ isReadonly: false,
224
+ key: 'auth.old',
225
+ module: 'auth'
226
+ });
227
+ mockRepo.find.mockResolvedValue([
228
+ existing
229
+ ]);
230
+ qb.getOne.mockResolvedValue(buildTranslationKey({
231
+ id: 'tkey-other',
232
+ key: 'auth.new'
233
+ }));
234
+ await expect(service.update({
235
+ id: 'tkey-1',
236
+ key: 'auth.new'
237
+ }, (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
238
+ response: expect.objectContaining({
239
+ messageKey: 'translation.key.duplicate.key.in.module'
240
+ })
241
+ });
242
+ // excludeId must be used so the row being updated doesn't collide with itself
243
+ expect(qb.andWhere).toHaveBeenCalledWith('tk.id != :excludeId', {
244
+ excludeId: 'tkey-1'
245
+ });
246
+ });
247
+ });
248
+ describe('beforeDeleteOperation (via delete)', ()=>{
249
+ it('rejects with ForbiddenException + messageKey when any target id is readonly', async ()=>{
250
+ const readonlyKey = buildTranslationKey({
251
+ id: 'tkey-1',
252
+ isReadonly: true,
253
+ key: 'system.core'
254
+ });
255
+ mockRepo.find.mockResolvedValue([
256
+ readonlyKey
257
+ ]);
258
+ const dto = {
259
+ id: [
260
+ 'tkey-1'
261
+ ],
262
+ type: 'delete'
263
+ };
264
+ await expect(service.delete(dto, (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
265
+ response: expect.objectContaining({
266
+ messageKey: 'translation.key.readonly.delete.forbidden'
267
+ })
268
+ });
269
+ });
270
+ it('allows delete when none of the targeted ids are readonly', async ()=>{
271
+ mockRepo.find.mockResolvedValue([]);
272
+ const dto = {
273
+ id: [
274
+ 'tkey-1'
275
+ ],
276
+ type: 'delete'
277
+ };
278
+ await expect(service.delete(dto, (0, _loggedusermock.buildMockUser)())).resolves.toBeNull();
279
+ expect(queryRunner.manager.softDelete).toHaveBeenCalled();
280
+ });
281
+ });
282
+ });