@flusys/nestjs-localization 7.1.0 → 8.0.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.
- package/config/index.d.ts +3 -3
- package/controllers/index.d.ts +3 -3
- package/controllers/language.controller.d.ts +4 -4
- package/controllers/translation-key.controller.d.ts +3 -3
- package/controllers/translation.controller.d.ts +4 -4
- package/docs/index.d.ts +1 -1
- package/docs/localization-swagger.config.d.ts +1 -1
- package/dtos/index.d.ts +3 -3
- package/dtos/translation.dto.d.ts +2 -2
- package/entities/index.d.ts +6 -6
- package/entities/language.entity.d.ts +1 -1
- package/entities/translation-key.entity.d.ts +1 -1
- package/entities/translation.entity.d.ts +2 -2
- package/fesm/349.js +1179 -0
- package/fesm/692.js +1205 -0
- package/fesm/config/index.js +101 -3
- package/fesm/controllers/index.js +15 -3
- package/fesm/docs/index.js +126 -1
- package/fesm/dtos/index.js +680 -3
- package/fesm/entities/index.js +405 -8
- package/fesm/index.js +257 -7
- package/fesm/interfaces/index.js +18 -4
- package/fesm/modules/index.js +152 -1
- package/fesm/runtime.js +85 -0
- package/fesm/services/index.js +15 -5
- package/index.d.ts +7 -7
- package/interfaces/index.d.ts +4 -4
- package/interfaces/language.interface.d.ts +1 -1
- package/interfaces/localization-module-options.interface.d.ts +3 -3
- package/interfaces/translation-key.interface.d.ts +1 -1
- package/interfaces/translation.interface.d.ts +3 -3
- package/modules/index.d.ts +1 -1
- package/modules/localization.module.d.ts +2 -2
- package/package.json +25 -34
- package/services/index.d.ts +5 -5
- package/services/language.service.d.ts +6 -6
- package/services/localization-config.service.d.ts +3 -3
- package/services/localization-datasource.provider.d.ts +3 -3
- package/services/translation-key.service.d.ts +6 -6
- package/services/translation.service.d.ts +5 -5
- package/cjs/config/event-actions.js +0 -33
- package/cjs/config/index.js +0 -20
- package/cjs/config/localization.constants.js +0 -11
- package/cjs/config/message-keys.js +0 -41
- package/cjs/controllers/index.js +0 -20
- package/cjs/controllers/language.controller.js +0 -188
- package/cjs/controllers/language.controller.spec.js +0 -154
- package/cjs/controllers/translation-key.controller.js +0 -135
- package/cjs/controllers/translation-key.controller.spec.js +0 -70
- package/cjs/controllers/translation.controller.js +0 -195
- package/cjs/controllers/translation.controller.spec.js +0 -169
- package/cjs/docs/index.js +0 -11
- package/cjs/docs/localization-swagger.config.js +0 -111
- package/cjs/docs/localization-swagger.config.spec.js +0 -30
- package/cjs/dtos/index.js +0 -20
- package/cjs/dtos/language.dto.js +0 -213
- package/cjs/dtos/translation-key.dto.js +0 -215
- package/cjs/dtos/translation.dto.js +0 -269
- package/cjs/entities/index.js +0 -42
- package/cjs/entities/index.spec.js +0 -18
- package/cjs/entities/language.entity.js +0 -126
- package/cjs/entities/translation-key.entity.js +0 -126
- package/cjs/entities/translation.entity.js +0 -120
- package/cjs/index.js +0 -24
- package/cjs/interfaces/index.js +0 -21
- package/cjs/interfaces/language.interface.js +0 -4
- package/cjs/interfaces/localization-module-options.interface.js +0 -4
- package/cjs/interfaces/translation-key.interface.js +0 -4
- package/cjs/interfaces/translation.interface.js +0 -4
- package/cjs/modules/index.js +0 -18
- package/cjs/modules/localization.module.js +0 -116
- package/cjs/modules/localization.module.spec.js +0 -132
- package/cjs/services/index.js +0 -22
- package/cjs/services/language.service.js +0 -181
- package/cjs/services/language.service.spec.js +0 -256
- package/cjs/services/localization-config.service.js +0 -66
- package/cjs/services/localization-config.service.spec.js +0 -80
- package/cjs/services/localization-datasource.provider.js +0 -79
- package/cjs/services/localization-datasource.provider.spec.js +0 -85
- package/cjs/services/translation-key.service.js +0 -179
- package/cjs/services/translation-key.service.spec.js +0 -303
- package/cjs/services/translation.service.js +0 -228
- package/cjs/services/translation.service.spec.js +0 -530
- package/fesm/config/event-actions.js +0 -12
- package/fesm/config/localization.constants.js +0 -1
- package/fesm/config/message-keys.js +0 -20
- package/fesm/controllers/language.controller.js +0 -178
- package/fesm/controllers/language.controller.spec.js +0 -150
- package/fesm/controllers/translation-key.controller.js +0 -125
- package/fesm/controllers/translation-key.controller.spec.js +0 -66
- package/fesm/controllers/translation.controller.js +0 -185
- package/fesm/controllers/translation.controller.spec.js +0 -165
- package/fesm/docs/localization-swagger.config.js +0 -101
- package/fesm/docs/localization-swagger.config.spec.js +0 -26
- package/fesm/dtos/language.dto.js +0 -192
- package/fesm/dtos/translation-key.dto.js +0 -194
- package/fesm/dtos/translation.dto.js +0 -239
- package/fesm/entities/index.spec.js +0 -14
- package/fesm/entities/language.entity.js +0 -116
- package/fesm/entities/translation-key.entity.js +0 -116
- package/fesm/entities/translation.entity.js +0 -110
- package/fesm/interfaces/language.interface.js +0 -1
- package/fesm/interfaces/localization-module-options.interface.js +0 -1
- package/fesm/interfaces/translation-key.interface.js +0 -1
- package/fesm/interfaces/translation.interface.js +0 -1
- package/fesm/modules/localization.module.js +0 -106
- package/fesm/modules/localization.module.spec.js +0 -128
- package/fesm/services/language.service.js +0 -171
- package/fesm/services/language.service.spec.js +0 -252
- package/fesm/services/localization-config.service.js +0 -56
- package/fesm/services/localization-config.service.spec.js +0 -76
- package/fesm/services/localization-datasource.provider.js +0 -69
- package/fesm/services/localization-datasource.provider.spec.js +0 -81
- package/fesm/services/translation-key.service.js +0 -169
- package/fesm/services/translation-key.service.spec.js +0 -299
- package/fesm/services/translation.service.js +0 -218
- package/fesm/services/translation.service.spec.js +0 -526
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
function _define_property(obj, key, value) {
|
|
2
|
-
if (key in obj) {
|
|
3
|
-
Object.defineProperty(obj, key, {
|
|
4
|
-
value: value,
|
|
5
|
-
enumerable: true,
|
|
6
|
-
configurable: true,
|
|
7
|
-
writable: true
|
|
8
|
-
});
|
|
9
|
-
} else {
|
|
10
|
-
obj[key] = value;
|
|
11
|
-
}
|
|
12
|
-
return obj;
|
|
13
|
-
}
|
|
14
|
-
function _ts_decorate(decorators, target, key, desc) {
|
|
15
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
17
|
-
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
18
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
|
-
}
|
|
20
|
-
function _ts_metadata(k, v) {
|
|
21
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
|
-
}
|
|
23
|
-
function _ts_param(paramIndex, decorator) {
|
|
24
|
-
return function(target, key) {
|
|
25
|
-
decorator(target, key, paramIndex);
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
|
|
29
|
-
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
-
import { BadRequestException, ForbiddenException, Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
|
-
import { In, IsNull } from 'typeorm';
|
|
32
|
-
import { LOCALIZATION_EVENT_ENTITIES, LOCALIZATION_EVENT_MODULE, TRANSLATION_KEY_MESSAGES } from '../config';
|
|
33
|
-
import { TranslationKey } from '../entities';
|
|
34
|
-
import { LocalizationDataSourceProvider } from './localization-datasource.provider';
|
|
35
|
-
export class TranslationKeyService extends ApiService {
|
|
36
|
-
async beforeInsertOperation(dto, _user, _queryRunner) {
|
|
37
|
-
const dtos = Array.isArray(dto) ? dto : [
|
|
38
|
-
dto
|
|
39
|
-
];
|
|
40
|
-
await this.validateNoDuplicateKeys(dtos.map((d)=>({
|
|
41
|
-
module: d.module,
|
|
42
|
-
key: d.key
|
|
43
|
-
})));
|
|
44
|
-
}
|
|
45
|
-
async beforeUpdateOperation(dto, _user, _queryRunner) {
|
|
46
|
-
const dtos = Array.isArray(dto) ? dto : [
|
|
47
|
-
dto
|
|
48
|
-
];
|
|
49
|
-
const ids = dtos.map((d)=>d.id);
|
|
50
|
-
await this.ensureDataSourceRepository();
|
|
51
|
-
const existingKeys = await this.repository.find({
|
|
52
|
-
where: {
|
|
53
|
-
id: In(ids),
|
|
54
|
-
deletedAt: IsNull()
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
// Check if any readonly keys are being updated with key/module changes
|
|
58
|
-
for (const d of dtos){
|
|
59
|
-
const existing = existingKeys.find((e)=>e.id === d.id);
|
|
60
|
-
const changesIdentity = d.key !== undefined && d.key !== existing?.key || d.module !== undefined && d.module !== existing?.module;
|
|
61
|
-
if (existing?.isReadonly && changesIdentity) {
|
|
62
|
-
throw new ForbiddenException({
|
|
63
|
-
message: `Cannot modify key or module of readonly translation key "${existing.key}"`,
|
|
64
|
-
messageKey: TRANSLATION_KEY_MESSAGES.READONLY_UPDATE_FORBIDDEN,
|
|
65
|
-
messageVariables: {
|
|
66
|
-
key: existing.key
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Validate no duplicates for keys being changed
|
|
72
|
-
const keysToValidate = dtos.filter((d)=>d.key || d.module).map((d)=>{
|
|
73
|
-
const existing = existingKeys.find((e)=>e.id === d.id);
|
|
74
|
-
return {
|
|
75
|
-
module: d.module ?? existing?.module ?? '',
|
|
76
|
-
key: d.key ?? existing?.key ?? '',
|
|
77
|
-
excludeId: d.id
|
|
78
|
-
};
|
|
79
|
-
});
|
|
80
|
-
if (keysToValidate.length > 0) {
|
|
81
|
-
await this.validateNoDuplicateKeys(keysToValidate);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
async beforeDeleteOperation(option, _user, _queryRunner) {
|
|
85
|
-
const ids = Array.isArray(option.id) ? option.id : [
|
|
86
|
-
option.id
|
|
87
|
-
];
|
|
88
|
-
await this.ensureDataSourceRepository();
|
|
89
|
-
const readonlyKeys = await this.repository.find({
|
|
90
|
-
where: {
|
|
91
|
-
id: In(ids),
|
|
92
|
-
isReadonly: true,
|
|
93
|
-
deletedAt: IsNull()
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
if (readonlyKeys.length > 0) {
|
|
97
|
-
const keys = readonlyKeys.map((k)=>k.key).join(', ');
|
|
98
|
-
throw new ForbiddenException({
|
|
99
|
-
message: `Cannot delete readonly translation keys: ${keys}`,
|
|
100
|
-
messageKey: TRANSLATION_KEY_MESSAGES.READONLY_DELETE_FORBIDDEN,
|
|
101
|
-
messageVariables: {
|
|
102
|
-
keys
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
async findByModule(module) {
|
|
108
|
-
await this.ensureDataSourceRepository();
|
|
109
|
-
return this.repository.find({
|
|
110
|
-
where: {
|
|
111
|
-
module,
|
|
112
|
-
isActive: true,
|
|
113
|
-
deletedAt: IsNull()
|
|
114
|
-
},
|
|
115
|
-
order: {
|
|
116
|
-
serial: 'ASC',
|
|
117
|
-
key: 'ASC'
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
async getModules() {
|
|
122
|
-
await this.ensureDataSourceRepository();
|
|
123
|
-
const result = await this.repository.createQueryBuilder('tk').select('DISTINCT tk.module', 'module').where('tk.deleted_at IS NULL').getRawMany();
|
|
124
|
-
return result.map((r)=>r.module);
|
|
125
|
-
}
|
|
126
|
-
async validateNoDuplicateKeys(keys) {
|
|
127
|
-
await this.ensureDataSourceRepository();
|
|
128
|
-
for (const { module, key, excludeId } of keys){
|
|
129
|
-
const query = this.repository.createQueryBuilder('tk').where('tk.module = :module', {
|
|
130
|
-
module
|
|
131
|
-
}).andWhere('tk.key = :key', {
|
|
132
|
-
key
|
|
133
|
-
}).andWhere('tk.deleted_at IS NULL');
|
|
134
|
-
if (excludeId) {
|
|
135
|
-
query.andWhere('tk.id != :excludeId', {
|
|
136
|
-
excludeId
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
const existing = await query.getOne();
|
|
140
|
-
if (existing) {
|
|
141
|
-
throw new BadRequestException({
|
|
142
|
-
message: `Translation key "${key}" already exists in module "${module}"`,
|
|
143
|
-
messageKey: TRANSLATION_KEY_MESSAGES.DUPLICATE_KEY_IN_MODULE,
|
|
144
|
-
messageVariables: {
|
|
145
|
-
key,
|
|
146
|
-
module
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
constructor(cacheManager, utilsService, dataSourceProvider){
|
|
153
|
-
super(LOCALIZATION_EVENT_ENTITIES.TRANSLATION_KEY, cacheManager, utilsService, TranslationKeyService.name, true, LOCALIZATION_EVENT_MODULE, dataSourceProvider, TranslationKey), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
TranslationKeyService = _ts_decorate([
|
|
157
|
-
Injectable({
|
|
158
|
-
scope: Scope.REQUEST
|
|
159
|
-
}),
|
|
160
|
-
_ts_param(0, Inject('CACHE_INSTANCE')),
|
|
161
|
-
_ts_param(1, Inject(UtilsService)),
|
|
162
|
-
_ts_param(2, Inject(LocalizationDataSourceProvider)),
|
|
163
|
-
_ts_metadata("design:type", Function),
|
|
164
|
-
_ts_metadata("design:paramtypes", [
|
|
165
|
-
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
166
|
-
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
167
|
-
typeof LocalizationDataSourceProvider === "undefined" ? Object : LocalizationDataSourceProvider
|
|
168
|
-
])
|
|
169
|
-
], TranslationKeyService);
|
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
import { Test } from '@nestjs/testing';
|
|
2
|
-
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
3
|
-
import { ForbiddenException, BadRequestException } from '@nestjs/common';
|
|
4
|
-
import { createMockDataSourceProvider, createMockRepository } from '@test-utils/mocks/repository.mock';
|
|
5
|
-
import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
|
|
6
|
-
import { TranslationKeyService } from './translation-key.service';
|
|
7
|
-
import { LocalizationDataSourceProvider } from './localization-datasource.provider';
|
|
8
|
-
function buildTranslationKey(overrides = {}) {
|
|
9
|
-
return {
|
|
10
|
-
id: 'tkey-uuid-1',
|
|
11
|
-
module: 'auth',
|
|
12
|
-
key: 'auth.login.success',
|
|
13
|
-
defaultMessage: 'Login successful',
|
|
14
|
-
description: null,
|
|
15
|
-
variables: null,
|
|
16
|
-
isActive: true,
|
|
17
|
-
isReadonly: false,
|
|
18
|
-
serial: 1,
|
|
19
|
-
createdAt: new Date(),
|
|
20
|
-
updatedAt: new Date(),
|
|
21
|
-
deletedAt: null,
|
|
22
|
-
createdById: null,
|
|
23
|
-
updatedById: null,
|
|
24
|
-
deletedById: null,
|
|
25
|
-
...overrides
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
describe('TranslationKeyService', ()=>{
|
|
29
|
-
let service;
|
|
30
|
-
let mockRepo;
|
|
31
|
-
let qb;
|
|
32
|
-
let queryRunner;
|
|
33
|
-
beforeEach(async ()=>{
|
|
34
|
-
mockRepo = createMockRepository();
|
|
35
|
-
qb = mockRepo.createQueryBuilder();
|
|
36
|
-
const mockDataSourceProvider = createMockDataSourceProvider(mockRepo);
|
|
37
|
-
// executeInTransaction()/delete() always grab a QueryRunner off repository.manager.connection
|
|
38
|
-
// before running the before*Operation hooks, so every test (success or rejection) needs one.
|
|
39
|
-
queryRunner = {
|
|
40
|
-
connect: jest.fn(),
|
|
41
|
-
startTransaction: jest.fn(),
|
|
42
|
-
commitTransaction: jest.fn(),
|
|
43
|
-
rollbackTransaction: jest.fn(),
|
|
44
|
-
release: jest.fn(),
|
|
45
|
-
manager: {
|
|
46
|
-
save: jest.fn(),
|
|
47
|
-
softDelete: jest.fn(),
|
|
48
|
-
update: jest.fn(),
|
|
49
|
-
delete: jest.fn(),
|
|
50
|
-
restore: jest.fn()
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
mockRepo.manager.connection.createQueryRunner.mockReturnValue(queryRunner);
|
|
54
|
-
const mockUtilsService = {
|
|
55
|
-
clearCache: jest.fn(),
|
|
56
|
-
getCacheKey: jest.fn().mockReturnValue('cache-key'),
|
|
57
|
-
trackCacheKey: jest.fn()
|
|
58
|
-
};
|
|
59
|
-
const module = await Test.createTestingModule({
|
|
60
|
-
providers: [
|
|
61
|
-
TranslationKeyService,
|
|
62
|
-
{
|
|
63
|
-
provide: 'CACHE_INSTANCE',
|
|
64
|
-
useValue: {
|
|
65
|
-
get: jest.fn(),
|
|
66
|
-
set: jest.fn()
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
provide: UtilsService,
|
|
71
|
-
useValue: mockUtilsService
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
provide: LocalizationDataSourceProvider,
|
|
75
|
-
useValue: mockDataSourceProvider
|
|
76
|
-
}
|
|
77
|
-
]
|
|
78
|
-
}).compile();
|
|
79
|
-
service = await module.resolve(TranslationKeyService);
|
|
80
|
-
});
|
|
81
|
-
describe('findByModule', ()=>{
|
|
82
|
-
it('returns active, non-deleted keys for the module ordered by serial then key', async ()=>{
|
|
83
|
-
const keys = [
|
|
84
|
-
buildTranslationKey({
|
|
85
|
-
key: 'auth.login.success',
|
|
86
|
-
serial: 1
|
|
87
|
-
})
|
|
88
|
-
];
|
|
89
|
-
mockRepo.find.mockResolvedValue(keys);
|
|
90
|
-
const result = await service.findByModule('auth');
|
|
91
|
-
expect(mockRepo.find).toHaveBeenCalledWith({
|
|
92
|
-
where: {
|
|
93
|
-
module: 'auth',
|
|
94
|
-
isActive: true,
|
|
95
|
-
deletedAt: expect.anything()
|
|
96
|
-
},
|
|
97
|
-
order: {
|
|
98
|
-
serial: 'ASC',
|
|
99
|
-
key: 'ASC'
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
expect(result).toEqual(keys);
|
|
103
|
-
});
|
|
104
|
-
it('returns an empty array for an unknown module', async ()=>{
|
|
105
|
-
mockRepo.find.mockResolvedValue([]);
|
|
106
|
-
const result = await service.findByModule('does-not-exist');
|
|
107
|
-
expect(result).toEqual([]);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
describe('getModules', ()=>{
|
|
111
|
-
it('returns distinct module names', async ()=>{
|
|
112
|
-
// `getRawMany` isn't part of the shared MockQueryBuilder — attach it for this raw-query path.
|
|
113
|
-
const getRawMany = jest.fn().mockResolvedValue([
|
|
114
|
-
{
|
|
115
|
-
module: 'auth'
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
module: 'shared'
|
|
119
|
-
}
|
|
120
|
-
]);
|
|
121
|
-
qb.getRawMany = getRawMany;
|
|
122
|
-
const result = await service.getModules();
|
|
123
|
-
expect(result).toEqual([
|
|
124
|
-
'auth',
|
|
125
|
-
'shared'
|
|
126
|
-
]);
|
|
127
|
-
});
|
|
128
|
-
it('returns an empty array when there are no translation keys yet', async ()=>{
|
|
129
|
-
const getRawMany = jest.fn().mockResolvedValue([]);
|
|
130
|
-
qb.getRawMany = getRawMany;
|
|
131
|
-
const result = await service.getModules();
|
|
132
|
-
expect(result).toEqual([]);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
describe('beforeInsertOperation (via insert) — duplicate key validation', ()=>{
|
|
136
|
-
it('allows insert when no key with the same module+key exists', async ()=>{
|
|
137
|
-
qb.getOne.mockResolvedValue(null);
|
|
138
|
-
const created = buildTranslationKey();
|
|
139
|
-
mockRepo.create.mockReturnValue({});
|
|
140
|
-
queryRunner.manager.save.mockResolvedValue(created);
|
|
141
|
-
await expect(service.insert({
|
|
142
|
-
module: 'auth',
|
|
143
|
-
key: 'auth.login.success',
|
|
144
|
-
defaultMessage: 'Login successful'
|
|
145
|
-
}, buildMockUser())).resolves.toBeDefined();
|
|
146
|
-
});
|
|
147
|
-
it('rejects with BadRequestException + messageKey when the module+key combination already exists', async ()=>{
|
|
148
|
-
qb.getOne.mockResolvedValue(buildTranslationKey());
|
|
149
|
-
await expect(service.insert({
|
|
150
|
-
module: 'auth',
|
|
151
|
-
key: 'auth.login.success',
|
|
152
|
-
defaultMessage: 'Login successful'
|
|
153
|
-
}, buildMockUser())).rejects.toMatchObject({
|
|
154
|
-
response: expect.objectContaining({
|
|
155
|
-
messageKey: 'translation.key.duplicate.key.in.module'
|
|
156
|
-
})
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
it('rejects with BadRequestException for duplicate module+key in insertMany', async ()=>{
|
|
160
|
-
qb.getOne.mockResolvedValueOnce(null).mockResolvedValueOnce(buildTranslationKey({
|
|
161
|
-
key: 'auth.logout.success'
|
|
162
|
-
}));
|
|
163
|
-
await expect(service.insertMany([
|
|
164
|
-
{
|
|
165
|
-
module: 'auth',
|
|
166
|
-
key: 'auth.login.success',
|
|
167
|
-
defaultMessage: 'A'
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
module: 'auth',
|
|
171
|
-
key: 'auth.logout.success',
|
|
172
|
-
defaultMessage: 'B'
|
|
173
|
-
}
|
|
174
|
-
], buildMockUser())).rejects.toBeInstanceOf(BadRequestException);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
describe('beforeUpdateOperation (via update)', ()=>{
|
|
178
|
-
it('rejects with ForbiddenException when modifying key/module of a readonly translation key', async ()=>{
|
|
179
|
-
const existing = buildTranslationKey({
|
|
180
|
-
id: 'tkey-1',
|
|
181
|
-
isReadonly: true,
|
|
182
|
-
key: 'system.ready'
|
|
183
|
-
});
|
|
184
|
-
mockRepo.find.mockResolvedValue([
|
|
185
|
-
existing
|
|
186
|
-
]);
|
|
187
|
-
await expect(service.update({
|
|
188
|
-
id: 'tkey-1',
|
|
189
|
-
key: 'system.changed'
|
|
190
|
-
}, buildMockUser())).rejects.toBeInstanceOf(ForbiddenException);
|
|
191
|
-
await expect(service.update({
|
|
192
|
-
id: 'tkey-1',
|
|
193
|
-
key: 'system.changed'
|
|
194
|
-
}, buildMockUser())).rejects.toMatchObject({
|
|
195
|
-
response: expect.objectContaining({
|
|
196
|
-
messageKey: 'translation.key.readonly.update.forbidden'
|
|
197
|
-
})
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
it('allows updating non key/module fields of a readonly translation key', async ()=>{
|
|
201
|
-
const existing = buildTranslationKey({
|
|
202
|
-
id: 'tkey-1',
|
|
203
|
-
isReadonly: true,
|
|
204
|
-
key: 'system.ready'
|
|
205
|
-
});
|
|
206
|
-
mockRepo.find.mockResolvedValue([
|
|
207
|
-
existing
|
|
208
|
-
]);
|
|
209
|
-
mockRepo.findOne.mockResolvedValue(existing);
|
|
210
|
-
queryRunner.manager.save.mockResolvedValue(existing);
|
|
211
|
-
await expect(service.update({
|
|
212
|
-
id: 'tkey-1',
|
|
213
|
-
defaultMessage: 'Updated readonly message'
|
|
214
|
-
}, buildMockUser())).resolves.toBeDefined();
|
|
215
|
-
});
|
|
216
|
-
it('allows resubmitting the same unchanged key/module of a readonly translation key', async ()=>{
|
|
217
|
-
// Regression test: the edit form always resends key + module even when only
|
|
218
|
-
// other fields changed, so the check must compare values, not just presence.
|
|
219
|
-
const existing = buildTranslationKey({
|
|
220
|
-
id: 'tkey-1',
|
|
221
|
-
isReadonly: true,
|
|
222
|
-
key: 'system.ready',
|
|
223
|
-
module: 'system'
|
|
224
|
-
});
|
|
225
|
-
mockRepo.find.mockResolvedValue([
|
|
226
|
-
existing
|
|
227
|
-
]);
|
|
228
|
-
mockRepo.findOne.mockResolvedValue(existing);
|
|
229
|
-
queryRunner.manager.save.mockResolvedValue(existing);
|
|
230
|
-
await expect(service.update({
|
|
231
|
-
id: 'tkey-1',
|
|
232
|
-
key: 'system.ready',
|
|
233
|
-
module: 'system',
|
|
234
|
-
defaultMessage: 'Updated readonly message'
|
|
235
|
-
}, buildMockUser())).resolves.toBeDefined();
|
|
236
|
-
});
|
|
237
|
-
it('rejects with BadRequestException when the new key/module already exists on another row', async ()=>{
|
|
238
|
-
const existing = buildTranslationKey({
|
|
239
|
-
id: 'tkey-1',
|
|
240
|
-
isReadonly: false,
|
|
241
|
-
key: 'auth.old',
|
|
242
|
-
module: 'auth'
|
|
243
|
-
});
|
|
244
|
-
mockRepo.find.mockResolvedValue([
|
|
245
|
-
existing
|
|
246
|
-
]);
|
|
247
|
-
qb.getOne.mockResolvedValue(buildTranslationKey({
|
|
248
|
-
id: 'tkey-other',
|
|
249
|
-
key: 'auth.new'
|
|
250
|
-
}));
|
|
251
|
-
await expect(service.update({
|
|
252
|
-
id: 'tkey-1',
|
|
253
|
-
key: 'auth.new'
|
|
254
|
-
}, buildMockUser())).rejects.toMatchObject({
|
|
255
|
-
response: expect.objectContaining({
|
|
256
|
-
messageKey: 'translation.key.duplicate.key.in.module'
|
|
257
|
-
})
|
|
258
|
-
});
|
|
259
|
-
// excludeId must be used so the row being updated doesn't collide with itself
|
|
260
|
-
expect(qb.andWhere).toHaveBeenCalledWith('tk.id != :excludeId', {
|
|
261
|
-
excludeId: 'tkey-1'
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
describe('beforeDeleteOperation (via delete)', ()=>{
|
|
266
|
-
it('rejects with ForbiddenException + messageKey when any target id is readonly', async ()=>{
|
|
267
|
-
const readonlyKey = buildTranslationKey({
|
|
268
|
-
id: 'tkey-1',
|
|
269
|
-
isReadonly: true,
|
|
270
|
-
key: 'system.core'
|
|
271
|
-
});
|
|
272
|
-
mockRepo.find.mockResolvedValue([
|
|
273
|
-
readonlyKey
|
|
274
|
-
]);
|
|
275
|
-
const dto = {
|
|
276
|
-
id: [
|
|
277
|
-
'tkey-1'
|
|
278
|
-
],
|
|
279
|
-
type: 'delete'
|
|
280
|
-
};
|
|
281
|
-
await expect(service.delete(dto, buildMockUser())).rejects.toMatchObject({
|
|
282
|
-
response: expect.objectContaining({
|
|
283
|
-
messageKey: 'translation.key.readonly.delete.forbidden'
|
|
284
|
-
})
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
it('allows delete when none of the targeted ids are readonly', async ()=>{
|
|
288
|
-
mockRepo.find.mockResolvedValue([]);
|
|
289
|
-
const dto = {
|
|
290
|
-
id: [
|
|
291
|
-
'tkey-1'
|
|
292
|
-
],
|
|
293
|
-
type: 'delete'
|
|
294
|
-
};
|
|
295
|
-
await expect(service.delete(dto, buildMockUser())).resolves.toBeNull();
|
|
296
|
-
expect(queryRunner.manager.softDelete).toHaveBeenCalled();
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
});
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
function _define_property(obj, key, value) {
|
|
2
|
-
if (key in obj) {
|
|
3
|
-
Object.defineProperty(obj, key, {
|
|
4
|
-
value: value,
|
|
5
|
-
enumerable: true,
|
|
6
|
-
configurable: true,
|
|
7
|
-
writable: true
|
|
8
|
-
});
|
|
9
|
-
} else {
|
|
10
|
-
obj[key] = value;
|
|
11
|
-
}
|
|
12
|
-
return obj;
|
|
13
|
-
}
|
|
14
|
-
function _ts_decorate(decorators, target, key, desc) {
|
|
15
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
17
|
-
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
18
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
|
-
}
|
|
20
|
-
function _ts_metadata(k, v) {
|
|
21
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
|
-
}
|
|
23
|
-
function _ts_param(paramIndex, decorator) {
|
|
24
|
-
return function(target, key) {
|
|
25
|
-
decorator(target, key, paramIndex);
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
|
|
29
|
-
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
-
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
|
-
import { In, IsNull } from 'typeorm';
|
|
32
|
-
import { LOCALIZATION_EVENT_ACTIONS, LOCALIZATION_EVENT_ENTITIES, LOCALIZATION_EVENT_MODULE } from '../config';
|
|
33
|
-
import { Language, Translation, TranslationKey } from '../entities';
|
|
34
|
-
import { LocalizationDataSourceProvider } from './localization-datasource.provider';
|
|
35
|
-
export class TranslationService extends ApiService {
|
|
36
|
-
async getFilterQuery(query, filter, _user) {
|
|
37
|
-
// Handle module filter by joining to translation_key table
|
|
38
|
-
if (filter.module) {
|
|
39
|
-
query.innerJoin('translation.translationKey', 'tk');
|
|
40
|
-
query.andWhere('tk.module = :module', {
|
|
41
|
-
module: filter.module
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
// Handle standard filters on translation table
|
|
45
|
-
if (filter.languageId) {
|
|
46
|
-
query.andWhere('translation.languageId = :languageId', {
|
|
47
|
-
languageId: filter.languageId
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
if (filter.translationKeyId) {
|
|
51
|
-
query.andWhere('translation.translationKeyId = :translationKeyId', {
|
|
52
|
-
translationKeyId: filter.translationKeyId
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
query,
|
|
57
|
-
isRaw: false
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
async ensureAllReposInitialized() {
|
|
61
|
-
await this.ensureDataSourceRepository();
|
|
62
|
-
if (!this.languageRepo) {
|
|
63
|
-
const ds = await this.dataSourceProvider.getDataSource();
|
|
64
|
-
this.languageRepo = ds.getRepository(Language);
|
|
65
|
-
this.translationKeyRepo = ds.getRepository(TranslationKey);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
async getByLanguageCode(languageCode, modules) {
|
|
69
|
-
await this.ensureAllReposInitialized();
|
|
70
|
-
const language = await this.findActiveLanguageByCode(languageCode);
|
|
71
|
-
if (!language) return {};
|
|
72
|
-
const qb = this.repository.createQueryBuilder('t').innerJoinAndSelect('t.translationKey', 'tk').where('t.language_id = :languageId', {
|
|
73
|
-
languageId: language.id
|
|
74
|
-
}).andWhere('t.is_active = true').andWhere('t.deleted_at IS NULL').andWhere('tk.is_active = true').andWhere('tk.deleted_at IS NULL');
|
|
75
|
-
if (modules && modules.length > 0) {
|
|
76
|
-
qb.andWhere('tk.module IN (:...modules)', {
|
|
77
|
-
modules
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
return this.toTranslationsMap(await qb.getMany());
|
|
81
|
-
}
|
|
82
|
-
async getByModuleAndLanguage(module, languageCode) {
|
|
83
|
-
await this.ensureAllReposInitialized();
|
|
84
|
-
const language = await this.findActiveLanguageByCode(languageCode);
|
|
85
|
-
if (!language) return {};
|
|
86
|
-
const translations = await this.repository.createQueryBuilder('t').innerJoinAndSelect('t.translationKey', 'tk').where('t.language_id = :languageId', {
|
|
87
|
-
languageId: language.id
|
|
88
|
-
}).andWhere('tk.module = :module', {
|
|
89
|
-
module
|
|
90
|
-
}).andWhere('t.is_active = true').andWhere('t.deleted_at IS NULL').andWhere('tk.is_active = true').andWhere('tk.deleted_at IS NULL').getMany();
|
|
91
|
-
return this.toTranslationsMap(translations);
|
|
92
|
-
}
|
|
93
|
-
async bulkUpdate(updates) {
|
|
94
|
-
await this.ensureDataSourceRepository();
|
|
95
|
-
if (updates.length === 0) return [];
|
|
96
|
-
const ds = await this.dataSourceProvider.getDataSource();
|
|
97
|
-
const queryRunner = ds.createQueryRunner();
|
|
98
|
-
await queryRunner.connect();
|
|
99
|
-
await queryRunner.startTransaction();
|
|
100
|
-
try {
|
|
101
|
-
const resultIds = [];
|
|
102
|
-
for (const update of updates){
|
|
103
|
-
if (update.id) {
|
|
104
|
-
// Update existing translation
|
|
105
|
-
await queryRunner.manager.update(Translation, {
|
|
106
|
-
id: update.id
|
|
107
|
-
}, {
|
|
108
|
-
value: update.value,
|
|
109
|
-
isVerified: update.isVerified ?? false
|
|
110
|
-
});
|
|
111
|
-
resultIds.push(update.id);
|
|
112
|
-
} else {
|
|
113
|
-
// Create new translation
|
|
114
|
-
const newTranslation = queryRunner.manager.create(Translation, {
|
|
115
|
-
languageId: update.languageId,
|
|
116
|
-
translationKeyId: update.translationKeyId,
|
|
117
|
-
value: update.value,
|
|
118
|
-
isVerified: update.isVerified ?? false,
|
|
119
|
-
isActive: true
|
|
120
|
-
});
|
|
121
|
-
const saved = await queryRunner.manager.save(newTranslation);
|
|
122
|
-
resultIds.push(saved.id);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
await queryRunner.commitTransaction();
|
|
126
|
-
const saved = await this.repository.find({
|
|
127
|
-
where: {
|
|
128
|
-
id: In(resultIds)
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
await Promise.all([
|
|
132
|
-
this.clearCacheForAll(),
|
|
133
|
-
this.clearCacheForId(saved)
|
|
134
|
-
]);
|
|
135
|
-
await this.publishDomainAction(LOCALIZATION_EVENT_ACTIONS.BULK_UPDATED, {
|
|
136
|
-
ids: saved.map((translation)=>translation.id),
|
|
137
|
-
metadata: {
|
|
138
|
-
count: saved.length
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
return saved;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
await queryRunner.rollbackTransaction();
|
|
144
|
-
throw error;
|
|
145
|
-
} finally{
|
|
146
|
-
await queryRunner.release();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
async getMissingTranslations(languageCode) {
|
|
150
|
-
await this.ensureAllReposInitialized();
|
|
151
|
-
const language = await this.findActiveLanguageByCode(languageCode);
|
|
152
|
-
if (!language) return [];
|
|
153
|
-
if (!this.translationKeyRepo) return [];
|
|
154
|
-
const allKeys = await this.translationKeyRepo.find({
|
|
155
|
-
where: {
|
|
156
|
-
isActive: true,
|
|
157
|
-
deletedAt: IsNull()
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
const existingTranslations = await this.repository.find({
|
|
161
|
-
where: {
|
|
162
|
-
languageId: language.id,
|
|
163
|
-
isActive: true,
|
|
164
|
-
deletedAt: IsNull()
|
|
165
|
-
},
|
|
166
|
-
select: [
|
|
167
|
-
'translationKeyId'
|
|
168
|
-
]
|
|
169
|
-
});
|
|
170
|
-
const existingKeyIds = new Set(existingTranslations.map((t)=>t.translationKeyId));
|
|
171
|
-
return allKeys.filter((k)=>!existingKeyIds.has(k.id));
|
|
172
|
-
}
|
|
173
|
-
async getTranslationCounts() {
|
|
174
|
-
await this.ensureDataSourceRepository();
|
|
175
|
-
const counts = await this.repository.createQueryBuilder('t').select('t.translation_key_id', 'keyId').addSelect('COUNT(t.id)', 'count').where('t.is_active = true').andWhere('t.deleted_at IS NULL').groupBy('t.translation_key_id').getRawMany();
|
|
176
|
-
const result = {};
|
|
177
|
-
for (const row of counts){
|
|
178
|
-
result[row.keyId] = parseInt(row.count, 10);
|
|
179
|
-
}
|
|
180
|
-
return result;
|
|
181
|
-
}
|
|
182
|
-
async findActiveLanguageByCode(code) {
|
|
183
|
-
if (!this.languageRepo) return null;
|
|
184
|
-
return this.languageRepo.findOne({
|
|
185
|
-
where: {
|
|
186
|
-
code,
|
|
187
|
-
isActive: true,
|
|
188
|
-
deletedAt: IsNull()
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
toTranslationsMap(translations) {
|
|
193
|
-
const result = {};
|
|
194
|
-
for (const t of translations){
|
|
195
|
-
if (t.translationKey) {
|
|
196
|
-
result[t.translationKey.key] = t.value;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return result;
|
|
200
|
-
}
|
|
201
|
-
constructor(cacheManager, utilsService, dataSourceProvider){
|
|
202
|
-
super(LOCALIZATION_EVENT_ENTITIES.TRANSLATION, cacheManager, utilsService, TranslationService.name, true, LOCALIZATION_EVENT_MODULE, dataSourceProvider, Translation), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "languageRepo", void 0), _define_property(this, "translationKeyRepo", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.languageRepo = null, this.translationKeyRepo = null;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
TranslationService = _ts_decorate([
|
|
206
|
-
Injectable({
|
|
207
|
-
scope: Scope.REQUEST
|
|
208
|
-
}),
|
|
209
|
-
_ts_param(0, Inject('CACHE_INSTANCE')),
|
|
210
|
-
_ts_param(1, Inject(UtilsService)),
|
|
211
|
-
_ts_param(2, Inject(LocalizationDataSourceProvider)),
|
|
212
|
-
_ts_metadata("design:type", Function),
|
|
213
|
-
_ts_metadata("design:paramtypes", [
|
|
214
|
-
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
215
|
-
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
216
|
-
typeof LocalizationDataSourceProvider === "undefined" ? Object : LocalizationDataSourceProvider
|
|
217
|
-
])
|
|
218
|
-
], TranslationService);
|