@ackplus/nest-dynamic-templates 0.1.50 → 1.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.
Files changed (148) hide show
  1. package/README.md +6 -284
  2. package/eslint.config.mjs +22 -0
  3. package/jest.config.ts +10 -0
  4. package/package.json +7 -58
  5. package/project.json +38 -0
  6. package/src/{index.d.ts → index.ts} +14 -1
  7. package/src/lib/constant.ts +2 -0
  8. package/src/lib/dto/create-template-layout.dto.ts +65 -0
  9. package/src/lib/dto/create-template.dto.ts +80 -0
  10. package/src/lib/dto/render-content-template-layout.dto.ts +32 -0
  11. package/src/lib/dto/render-content-template.dto.ts +37 -0
  12. package/src/lib/dto/render-template-layout.dto.ts +55 -0
  13. package/src/lib/dto/render-template.dto.ts +74 -0
  14. package/src/lib/dto/template-filter.dto.ts +52 -0
  15. package/src/lib/dto/template-layout-filter.dto.ts +51 -0
  16. package/src/lib/engines/language/html.engine.ts +49 -0
  17. package/src/lib/engines/language/markdown.engine.ts +37 -0
  18. package/src/lib/engines/language/mjml.engine.ts +44 -0
  19. package/src/lib/engines/language/text.engine.ts +15 -0
  20. package/src/lib/engines/{language-engine.d.ts → language-engine.ts} +7 -1
  21. package/src/lib/engines/template/ejs.engine.ts +33 -0
  22. package/src/lib/engines/template/handlebars.engine.ts +35 -0
  23. package/src/lib/engines/template/nunjucks.engine.ts +70 -0
  24. package/src/lib/engines/template/pug.engine.ts +43 -0
  25. package/src/lib/engines/{template-engine.d.ts → template-engine.ts} +6 -1
  26. package/src/lib/entities/template-layout.entity.ts +99 -0
  27. package/src/lib/entities/template.entity.ts +105 -0
  28. package/src/lib/errors/{template.errors.js → template.errors.ts} +20 -23
  29. package/src/lib/interfaces/{module-config.interface.d.ts → module-config.interface.ts} +17 -2
  30. package/src/lib/interfaces/template.types.ts +25 -0
  31. package/src/lib/nest-dynamic-templates.module.ts +143 -0
  32. package/src/lib/services/template-config.service.ts +112 -0
  33. package/src/lib/services/template-engine.registry.ts +109 -0
  34. package/src/lib/services/template-layout.service.ts +407 -0
  35. package/src/lib/services/template.service.ts +476 -0
  36. package/src/test/{helpers.js → helpers.ts} +5 -8
  37. package/src/test/nunjucks.service.spec.ts +157 -0
  38. package/src/test/pug.service.spec-temp +254 -0
  39. package/src/test/template-layout.service.spec.ts +422 -0
  40. package/src/test/template.service.spec.ts +862 -0
  41. package/src/test/test-database.config.ts +24 -0
  42. package/src/test/test-database.d.ts +6 -0
  43. package/src/test/test.setup.ts +34 -0
  44. package/src/types/ioredis.d.ts +6 -0
  45. package/src/types/mjml.d.ts +5 -0
  46. package/tsconfig.json +17 -0
  47. package/tsconfig.lib.json +14 -0
  48. package/tsconfig.spec.json +15 -0
  49. package/src/index.js +0 -23
  50. package/src/index.js.map +0 -1
  51. package/src/lib/constant.d.ts +0 -2
  52. package/src/lib/constant.js +0 -6
  53. package/src/lib/constant.js.map +0 -1
  54. package/src/lib/dto/create-template-layout.dto.d.ts +0 -15
  55. package/src/lib/dto/create-template-layout.dto.js +0 -87
  56. package/src/lib/dto/create-template-layout.dto.js.map +0 -1
  57. package/src/lib/dto/create-template.dto.d.ts +0 -17
  58. package/src/lib/dto/create-template.dto.js +0 -104
  59. package/src/lib/dto/create-template.dto.js.map +0 -1
  60. package/src/lib/dto/render-content-template-layout.dto.d.ts +0 -7
  61. package/src/lib/dto/render-content-template-layout.dto.js +0 -41
  62. package/src/lib/dto/render-content-template-layout.dto.js.map +0 -1
  63. package/src/lib/dto/render-content-template.dto.d.ts +0 -8
  64. package/src/lib/dto/render-content-template.dto.js +0 -47
  65. package/src/lib/dto/render-content-template.dto.js.map +0 -1
  66. package/src/lib/dto/render-template-layout.dto.d.ts +0 -10
  67. package/src/lib/dto/render-template-layout.dto.js +0 -68
  68. package/src/lib/dto/render-template-layout.dto.js.map +0 -1
  69. package/src/lib/dto/render-template.dto.d.ts +0 -14
  70. package/src/lib/dto/render-template.dto.js +0 -91
  71. package/src/lib/dto/render-template.dto.js.map +0 -1
  72. package/src/lib/dto/template-filter.dto.d.ts +0 -8
  73. package/src/lib/dto/template-filter.dto.js +0 -62
  74. package/src/lib/dto/template-filter.dto.js.map +0 -1
  75. package/src/lib/dto/template-layout-filter.dto.d.ts +0 -7
  76. package/src/lib/dto/template-layout-filter.dto.js +0 -61
  77. package/src/lib/dto/template-layout-filter.dto.js.map +0 -1
  78. package/src/lib/engines/language/html.engine.d.ts +0 -9
  79. package/src/lib/engines/language/html.engine.js +0 -79
  80. package/src/lib/engines/language/html.engine.js.map +0 -1
  81. package/src/lib/engines/language/index.js +0 -12
  82. package/src/lib/engines/language/index.js.map +0 -1
  83. package/src/lib/engines/language/markdown.engine.d.ts +0 -9
  84. package/src/lib/engines/language/markdown.engine.js +0 -26
  85. package/src/lib/engines/language/markdown.engine.js.map +0 -1
  86. package/src/lib/engines/language/mjml.engine.d.ts +0 -9
  87. package/src/lib/engines/language/mjml.engine.js +0 -76
  88. package/src/lib/engines/language/mjml.engine.js.map +0 -1
  89. package/src/lib/engines/language/text.engine.d.ts +0 -7
  90. package/src/lib/engines/language/text.engine.js +0 -16
  91. package/src/lib/engines/language/text.engine.js.map +0 -1
  92. package/src/lib/engines/language-engine.js +0 -7
  93. package/src/lib/engines/language-engine.js.map +0 -1
  94. package/src/lib/engines/template/ejs.engine.d.ts +0 -10
  95. package/src/lib/engines/template/ejs.engine.js +0 -66
  96. package/src/lib/engines/template/ejs.engine.js.map +0 -1
  97. package/src/lib/engines/template/handlebars.engine.d.ts +0 -10
  98. package/src/lib/engines/template/handlebars.engine.js +0 -67
  99. package/src/lib/engines/template/handlebars.engine.js.map +0 -1
  100. package/src/lib/engines/template/index.js +0 -12
  101. package/src/lib/engines/template/index.js.map +0 -1
  102. package/src/lib/engines/template/nunjucks.engine.d.ts +0 -16
  103. package/src/lib/engines/template/nunjucks.engine.js +0 -63
  104. package/src/lib/engines/template/nunjucks.engine.js.map +0 -1
  105. package/src/lib/engines/template/pug.engine.d.ts +0 -12
  106. package/src/lib/engines/template/pug.engine.js +0 -75
  107. package/src/lib/engines/template/pug.engine.js.map +0 -1
  108. package/src/lib/engines/template-engine.js +0 -7
  109. package/src/lib/engines/template-engine.js.map +0 -1
  110. package/src/lib/entities/template-layout.entity.d.ts +0 -20
  111. package/src/lib/entities/template-layout.entity.js +0 -121
  112. package/src/lib/entities/template-layout.entity.js.map +0 -1
  113. package/src/lib/entities/template.entity.d.ts +0 -21
  114. package/src/lib/entities/template.entity.js +0 -128
  115. package/src/lib/entities/template.entity.js.map +0 -1
  116. package/src/lib/errors/template.errors.d.ts +0 -19
  117. package/src/lib/errors/template.errors.js.map +0 -1
  118. package/src/lib/interfaces/module-config.interface.js +0 -3
  119. package/src/lib/interfaces/module-config.interface.js.map +0 -1
  120. package/src/lib/interfaces/template.types.d.ts +0 -22
  121. package/src/lib/interfaces/template.types.js +0 -25
  122. package/src/lib/interfaces/template.types.js.map +0 -1
  123. package/src/lib/nest-dynamic-templates.module.d.ts +0 -10
  124. package/src/lib/nest-dynamic-templates.module.js +0 -131
  125. package/src/lib/nest-dynamic-templates.module.js.map +0 -1
  126. package/src/lib/services/template-config.service.d.ts +0 -18
  127. package/src/lib/services/template-config.service.js +0 -66
  128. package/src/lib/services/template-config.service.js.map +0 -1
  129. package/src/lib/services/template-engine.registry.d.ts +0 -21
  130. package/src/lib/services/template-engine.registry.js +0 -94
  131. package/src/lib/services/template-engine.registry.js.map +0 -1
  132. package/src/lib/services/template-layout.service.d.ts +0 -24
  133. package/src/lib/services/template-layout.service.js +0 -301
  134. package/src/lib/services/template-layout.service.js.map +0 -1
  135. package/src/lib/services/template.service.d.ts +0 -26
  136. package/src/lib/services/template.service.js +0 -366
  137. package/src/lib/services/template.service.js.map +0 -1
  138. package/src/test/helpers.d.ts +0 -4
  139. package/src/test/helpers.js.map +0 -1
  140. package/src/test/test-database.config.d.ts +0 -4
  141. package/src/test/test-database.config.js +0 -24
  142. package/src/test/test-database.config.js.map +0 -1
  143. package/src/test/test.setup.d.ts +0 -3
  144. package/src/test/test.setup.js +0 -29
  145. package/src/test/test.setup.js.map +0 -1
  146. package/tsconfig.tsbuildinfo +0 -1
  147. /package/src/lib/engines/language/{index.d.ts → index.ts} +0 -0
  148. /package/src/lib/engines/template/{index.d.ts → index.ts} +0 -0
@@ -0,0 +1,862 @@
1
+ import { DataSource, Repository } from 'typeorm';
2
+ import { createTestModule } from './test.setup';
3
+ import { TemplateService } from '../lib/services/template.service';
4
+ import { NestDynamicTemplate } from '../lib/entities/template.entity';
5
+ import { TemplateTypeEnum, TemplateEngineEnum, TemplateLanguageEnum } from '../lib/interfaces/template.types';
6
+ import { CreateTemplateDto } from '../lib/dto/create-template.dto';
7
+ import { ForbiddenException, NotFoundException } from '@nestjs/common';
8
+ import { NestDynamicTemplateLayout } from '../lib/entities/template-layout.entity';
9
+ import { TemplateLayoutService } from '../lib/services/template-layout.service';
10
+ import { engineFilters } from './helpers';
11
+
12
+ describe('TemplateService', () => {
13
+ let service: TemplateService;
14
+ let layoutService: TemplateLayoutService;
15
+ let dataSource: DataSource;
16
+ let templateRepository: Repository<NestDynamicTemplate>;
17
+ let templateLayoutRepository: Repository<NestDynamicTemplateLayout>;
18
+
19
+ const testTemplate: CreateTemplateDto = {
20
+ name: 'test-template',
21
+ displayName: 'Test Template',
22
+ content: 'Hello {{name}}!',
23
+ type: TemplateTypeEnum.EMAIL,
24
+ engine: TemplateEngineEnum.NUNJUCKS,
25
+ language: TemplateLanguageEnum.HTML,
26
+ scope: 'system',
27
+ scopeId: null,
28
+ locale: 'en',
29
+ isActive: true
30
+ };
31
+
32
+ beforeEach(async () => {
33
+ const moduleRef = await createTestModule({
34
+ enginesOptions: {
35
+ filters: engineFilters
36
+ }
37
+ });
38
+
39
+ service = moduleRef.get<TemplateService>(TemplateService);
40
+ layoutService = moduleRef.get<TemplateLayoutService>(TemplateLayoutService);
41
+ dataSource = moduleRef.get<DataSource>(DataSource);
42
+ templateRepository = dataSource.getRepository(NestDynamicTemplate);
43
+ templateLayoutRepository = dataSource.getRepository(NestDynamicTemplateLayout);
44
+ });
45
+
46
+ afterEach(async () => {
47
+ // Clean up templates
48
+ await templateRepository.delete('1 = 1');
49
+
50
+ // Clean up database after each test
51
+ await dataSource.synchronize(true);
52
+ });
53
+
54
+ afterAll(async () => {
55
+ // Close the connection after all tests
56
+ await dataSource.destroy();
57
+ });
58
+
59
+ describe('System Template Management', () => {
60
+ it('should create system template', async () => {
61
+ const result = await service.createTemplate(testTemplate);
62
+
63
+ expect(result).toBeDefined();
64
+ expect(result.name).toBe(testTemplate.name);
65
+ expect(result.scope).toBe('system');
66
+ expect(result.scopeId).toBeNull();
67
+
68
+ // Verify template was saved in database
69
+ const savedTemplate = await templateRepository.findOne({ where: { id: result.id } });
70
+ expect(savedTemplate).toBeDefined();
71
+ expect(savedTemplate.name).toBe(testTemplate.name);
72
+ });
73
+
74
+ it('should not create other then system template', async () => {
75
+ await expect(service.createTemplate({
76
+ ...testTemplate,
77
+ scope: 'custom',
78
+ scopeId: '123',
79
+ content: 'Custom content for {{name}}'
80
+ })).rejects.toThrow(ForbiddenException);
81
+ });
82
+
83
+ it('should find system template', async () => {
84
+ // First create a template
85
+ const created = await service.createTemplate(testTemplate);
86
+
87
+ // Then find it
88
+ const found = await service.findTemplate(
89
+ testTemplate.name,
90
+ testTemplate.scope,
91
+ testTemplate.scopeId,
92
+ );
93
+
94
+ expect(found).toBeDefined();
95
+ expect(found.id).toBe(created.id);
96
+ expect(found.name).toBe(testTemplate.name);
97
+ expect(found.scope).toBe(testTemplate.scope);
98
+ });
99
+
100
+ it('should find system template when template not found with given scope', async () => {
101
+ // Create system template
102
+ const created = await service.createTemplate(testTemplate);
103
+
104
+ // Then find it with custom scope
105
+ const found = await service.findTemplate(
106
+ testTemplate.name,
107
+ 'custom',
108
+ '123',
109
+ );
110
+
111
+ // Verify template was found and is the system template
112
+ expect(found).toBeDefined();
113
+ expect(found).not.toBeNull();
114
+ expect(found.id).toBe(created.id);
115
+ expect(found.name).toBe(testTemplate.name);
116
+ expect(found.scope).toBe('system');
117
+ expect(found.scopeId).toBeNull();
118
+ });
119
+
120
+ it('should not find template when template not found in any scope and system', async () => {
121
+ // Try to find non-existent template
122
+ const result = await service.findTemplate(
123
+ 'non-existent-template',
124
+ 'custom',
125
+ '123',
126
+ );
127
+
128
+ expect(result).toBeNull();
129
+ });
130
+
131
+ it('should not find template when template not found in system scope', async () => {
132
+ // Try to find non-existent template in system scope
133
+ const result = await service.findTemplate(
134
+ 'non-existent-template',
135
+ 'system',
136
+ null,
137
+ );
138
+
139
+ expect(result).toBeNull();
140
+ });
141
+
142
+ it('should update system template when canUpdateSystemTemplate is true', async () => {
143
+ // First create a template
144
+ const created = await service.createTemplate(testTemplate);
145
+
146
+ // Then update it
147
+ const result = await service.updateTemplate(created.id, {
148
+ content: 'Updated system content for {{name}}'
149
+ }, true);
150
+
151
+ expect(result).toBeDefined();
152
+ expect(result.content).toBe('Updated system content for {{name}}');
153
+ expect(result.scope).toBe('system');
154
+
155
+ // Verify update in database
156
+ const updatedTemplate = await templateRepository.findOne({ where: { id: created.id } });
157
+ expect(updatedTemplate.content).toBe('Updated system content for {{name}}');
158
+ });
159
+
160
+ it('should not update system template when canUpdateSystemTemplate is false, it should throw ForbiddenException', async () => {
161
+ // First create a template
162
+ const created = await service.createTemplate(testTemplate);
163
+
164
+ // Then update it with canUpdateSystemTemplate set to false
165
+ await expect(service.updateTemplate(created.id, {
166
+ content: 'Updated system content for {{name}}'
167
+ }, false)).rejects.toThrow(ForbiddenException);
168
+
169
+
170
+ // Verify template was not updated
171
+ const updatedTemplate = await templateRepository.findOne({ where: { id: created.id } });
172
+ expect(updatedTemplate.content).toBe(testTemplate.content);
173
+ });
174
+
175
+
176
+ it('should not update system template when canUpdateSystemTemplate is false, but scope is not system then overwrite the template', async () => {
177
+ // First create a template
178
+ const created = await service.createTemplate(testTemplate);
179
+
180
+ // Then update it with canUpdateSystemTemplate set to false
181
+ await service.updateTemplate(created.id, {
182
+ scope: 'custom',
183
+ scopeId: '123',
184
+ content: 'Updated system content for {{name}}'
185
+ }, false);
186
+
187
+
188
+ // Verify system template was not updated
189
+ const updatedTemplate = await templateRepository.findOne({ where: { id: created.id } });
190
+ expect(updatedTemplate.content).toBe(testTemplate.content);
191
+
192
+ // Verify overwrite template was created
193
+ const overwrittenTemplate = await service.findTemplate(
194
+ testTemplate.name,
195
+ 'custom',
196
+ '123',
197
+ );
198
+ expect(overwrittenTemplate.content).toBe('Updated system content for {{name}}');
199
+ });
200
+
201
+ it('should delete system template when canDeleteSystemTemplate is true', async () => {
202
+ // First create a template
203
+ const created = await service.createTemplate(testTemplate);
204
+
205
+ // Then delete it
206
+ await service.deleteTemplate(created.id, true);
207
+
208
+ // Verify template was deleted
209
+ const deletedTemplate = await templateRepository.findOne({ where: { id: created.id } });
210
+ expect(deletedTemplate).toBeNull();
211
+ });
212
+
213
+ it('should not delete system template when canDeleteSystemTemplate is false, it should throw ForbiddenException', async () => {
214
+ // First create a template
215
+ const created = await service.createTemplate(testTemplate);
216
+
217
+ // Then delete it
218
+ await expect(service.deleteTemplate(created.id, false)).rejects.toThrow(ForbiddenException);
219
+
220
+ // Verify template was not deleted
221
+ const deletedTemplate = await templateRepository.findOne({ where: { id: created.id } });
222
+ expect(deletedTemplate).toBeDefined();
223
+ });
224
+ });
225
+
226
+ describe('Render System Template', () => {
227
+ it('should render system template', async () => {
228
+ // Create system template
229
+ await service.createTemplate(testTemplate);
230
+
231
+ // Render template
232
+ const renderedResult = await service.render({
233
+ name: testTemplate.name,
234
+ scope: testTemplate.scope,
235
+ scopeId: testTemplate.scopeId,
236
+ context: { name: 'John' }
237
+ });
238
+
239
+ // Verify template was rendered correctly
240
+ expect(renderedResult).toEqual({
241
+ subject: null,
242
+ content: 'Hello John!'
243
+ });
244
+ });
245
+
246
+ it('should not render system template when not found', async () => {
247
+ // Render template
248
+ await expect(service.render({
249
+ name: testTemplate.name,
250
+ scope: testTemplate.scope,
251
+ scopeId: testTemplate.scopeId,
252
+ context: { name: 'John' }
253
+ })).rejects.toThrow(NotFoundException);
254
+ });
255
+
256
+ it('should render system template even scope not system and template not found with given scope', async () => {
257
+ // Create system template
258
+ await service.createTemplate(testTemplate);
259
+
260
+ // Render template
261
+ const renderedResult = await service.render({
262
+ name: testTemplate.name,
263
+ scope: 'custom',
264
+ scopeId: '123',
265
+ context: { name: 'John' }
266
+ });
267
+
268
+ expect(renderedResult).toEqual({
269
+ subject: null,
270
+ content: 'Hello John!'
271
+ });
272
+ });
273
+ });
274
+
275
+ describe('Custom Scope Template Management', () => {
276
+ it('should create overwrite template for custom scope', async () => {
277
+ // Create system template
278
+ const systemTemplate = await service.createTemplate(testTemplate);
279
+
280
+ // Then create overwrite template
281
+ const overwriteTemplate = await service.overwriteSystemTemplate(systemTemplate.id, {
282
+ ...testTemplate,
283
+ scope: 'custom',
284
+ scopeId: '123',
285
+ content: 'Custom content for {{name}}'
286
+ });
287
+
288
+ expect(overwriteTemplate).toBeDefined();
289
+ expect(overwriteTemplate.scope).toBe('custom');
290
+ expect(overwriteTemplate.scopeId).toBe('123');
291
+ expect(overwriteTemplate.content).toBe('Custom content for {{name}}');
292
+
293
+ // Verify system template is not affected by overwrite template
294
+ const oldSystemTemplate = await templateRepository.findOne({
295
+ where: { name: testTemplate.name, scope: 'system' }
296
+ });
297
+ expect(oldSystemTemplate).toBeDefined();
298
+
299
+ // Verify system template and overwrite template are different
300
+ expect(systemTemplate.id).not.toBe(overwriteTemplate.id);
301
+
302
+ // Verify system template content is not affected by overwrite template
303
+ expect(oldSystemTemplate.id).toBe(systemTemplate.id);
304
+ expect(oldSystemTemplate.content).toBe(systemTemplate.content);
305
+ });
306
+
307
+ it('should update overwrite template without affecting system template', async () => {
308
+ // Create system template
309
+ const systemTemplate = await service.createTemplate(testTemplate);
310
+
311
+ // Create overwrite template
312
+ const overwrite = await service.overwriteSystemTemplate(systemTemplate.id, {
313
+ ...testTemplate,
314
+ scope: 'custom',
315
+ scopeId: '123',
316
+ content: 'Custom content for {{name}}'
317
+ });
318
+
319
+ // Update overwrite template
320
+ const result = await service.updateTemplate(overwrite.id, {
321
+ content: 'Updated custom content for {{name}}'
322
+ });
323
+
324
+ expect(result).toBeDefined();
325
+ expect(result.scope).toBe('custom');
326
+ expect(result.scopeId).toBe('123');
327
+ expect(result.content).toBe('Updated custom content for {{name}}');
328
+
329
+ // Verify system template remains unchanged
330
+ const oldSystemTemplate = await templateRepository.findOne({
331
+ where: { name: testTemplate.name, scope: 'system' }
332
+ });
333
+ expect(oldSystemTemplate.content).toBe(systemTemplate.content);
334
+ });
335
+
336
+ it('should not overwrite already overwritten template, it should return the already overwritten template but with updated fields', async () => {
337
+ // Create system template
338
+ const systemTemplate = await service.createTemplate(testTemplate);
339
+
340
+ // Create overwrite template
341
+ const overwrittenTemplate = await service.overwriteSystemTemplate(systemTemplate.id, {
342
+ ...testTemplate,
343
+ scope: 'custom',
344
+ scopeId: '123',
345
+ content: 'Custom content for {{name}}'
346
+ });
347
+
348
+ // Try to overwrite again
349
+ const result = await service.overwriteSystemTemplate(systemTemplate.id, {
350
+ ...testTemplate,
351
+ scope: 'custom',
352
+ scopeId: '123',
353
+ content: 'Updated custom content for {{name}}'
354
+ })
355
+
356
+ // Verify the result is the same as the already overwritten template
357
+ expect(result).toBeDefined();
358
+ expect(result.id).toBe(overwrittenTemplate.id);
359
+ expect(result.content).toBe('Updated custom content for {{name}}');
360
+ });
361
+
362
+ it('should find overwrite template when available', async () => {
363
+ // Create system template
364
+ const systemTemplate = await service.createTemplate(testTemplate);
365
+
366
+ // Create overwrite template
367
+ await service.overwriteSystemTemplate(systemTemplate.id, {
368
+ ...testTemplate,
369
+ scope: 'custom',
370
+ scopeId: '123',
371
+ content: 'Custom content for {{name}}'
372
+ });
373
+
374
+ // Find overwrite template
375
+ const found = await service.findTemplate(
376
+ testTemplate.name,
377
+ 'custom',
378
+ '123',
379
+ testTemplate.language
380
+ );
381
+
382
+ expect(found).toBeDefined();
383
+ expect(found.content).toBe('Custom content for {{name}}');
384
+ expect(found.scope).toBe('custom');
385
+ expect(found.scopeId).toBe('123');
386
+ })
387
+
388
+ it('should render overwrite template when available', async () => {
389
+ // Create system template
390
+ const systemTemplate = await service.createTemplate(testTemplate);
391
+
392
+ // Create overwrite template
393
+ await service.overwriteSystemTemplate(systemTemplate.id, {
394
+ ...testTemplate,
395
+ scope: 'custom',
396
+ scopeId: '123',
397
+ content: 'Custom content for {{name}}'
398
+ });
399
+
400
+ const result = await service.render({
401
+ name: testTemplate.name,
402
+ scope: 'custom',
403
+ scopeId: '123',
404
+ locale: testTemplate.language,
405
+ context: { name: 'John' }
406
+ });
407
+
408
+ expect(result).toEqual({
409
+ subject: null,
410
+ content: 'Custom content for John'
411
+ });
412
+ });
413
+
414
+ it('should not fail render when overwrite not found', async () => {
415
+ // Create only system template
416
+ await service.createTemplate(testTemplate);
417
+
418
+ const result = await service.render({
419
+ name: testTemplate.name,
420
+ scope: 'custom',
421
+ scopeId: '123',
422
+ locale: testTemplate.language,
423
+ context: { name: 'John' }
424
+ });
425
+
426
+ expect(result).toEqual({
427
+ subject: null,
428
+ content: 'Hello John!'
429
+ });
430
+ });
431
+ });
432
+
433
+ describe('Template with Layout', () => {
434
+ it('should render template with layout', async () => {
435
+ // Create layout template
436
+ const layoutTemplate = await layoutService.createTemplateLayout({
437
+ ...testTemplate,
438
+ name: 'test-layout',
439
+ content: '<!DOCTYPE html>\n<html>\n<head>\n <title>{{ title }}</title>\n</head>\n<body>\n <header>{{ header }}</header>\n <main>{{ content }}</main>\n <footer>{{ footer }}</footer>\n</body>\n</html>'
440
+ });
441
+
442
+ // Create content template with layout
443
+ const contentTemplate = await service.createTemplate({
444
+ ...testTemplate,
445
+ name: 'test-content',
446
+ content: 'Hello World',
447
+ templateLayoutName: 'test-layout'
448
+ });
449
+
450
+ const result = await service.render({
451
+ name: 'test-content',
452
+ scope: 'system',
453
+ context: {
454
+ title: 'Test Page',
455
+ header: 'Welcome',
456
+ footer: '© 2024'
457
+ }
458
+ });
459
+
460
+ expect(result.content).toMatch(/<!DOCTYPE html>/);
461
+ expect(result.content).toMatch(/<title>Test Page<\/title>/);
462
+ expect(result.content).toMatch(/<header>Welcome<\/header>/);
463
+ expect(result.content).toMatch(/<main>Hello World<\/main>/);
464
+ expect(result.content).toMatch(/<footer>© 2024<\/footer>/);
465
+ });
466
+
467
+ it('should render template with layout and filters', async () => {
468
+ // Create layout template
469
+ const layoutTemplate = await layoutService.createTemplateLayout({
470
+ ...testTemplate,
471
+ name: 'filter-layout',
472
+ content: '<!DOCTYPE html>\n<html>\n<head>\n <title>{{ date | formatDate("MMMM D, YYYY") }}</title>\n</head>\n<body>\n <header>Order {{ orderId }}</header>\n <main>{{ content }}</main>\n <footer>Total: {{ amount | formatCurrency("USD") }}</footer>\n</body>\n</html>'
473
+ });
474
+
475
+ // Create content template with layout
476
+ const contentTemplate = await service.createTemplate({
477
+ ...testTemplate,
478
+ name: 'filter-content',
479
+ content: 'Order placed on {{ date | formatDate("YYYY-MM-DD") }}',
480
+ templateLayoutName: 'filter-layout'
481
+ });
482
+
483
+ const date = new Date('2024-03-15');
484
+ const result = await service.render({
485
+ name: 'filter-content',
486
+ scope: 'system',
487
+ context: {
488
+ date: date,
489
+ orderId: '12345',
490
+ amount: 1234.56
491
+ }
492
+ });
493
+
494
+ expect(result.content).toMatch(/<!DOCTYPE html>/);
495
+ expect(result.content).toMatch(/<title>March 15, 2024<\/title>/);
496
+ expect(result.content).toMatch(/<header>Order 12345<\/header>/);
497
+ expect(result.content).toMatch(/<main>Order placed on 2024-03-15<\/main>/);
498
+ expect(result.content).toMatch(/<footer>Total: \$1,234\.56<\/footer>/);
499
+ });
500
+
501
+ it('should throw error when layout not found', async () => {
502
+ // Create content template with non-existent layout
503
+ const contentTemplate = await service.createTemplate({
504
+ ...testTemplate,
505
+ name: 'test-content',
506
+ content: 'Hello World',
507
+ templateLayoutName: 'non-existent-layout'
508
+ });
509
+
510
+ await expect(service.render({
511
+ name: 'test-content',
512
+ scope: 'system',
513
+ context: {}
514
+ })).rejects.toThrow(NotFoundException);
515
+ });
516
+
517
+ it('should handle layout with subject', async () => {
518
+ // Create layout template with subject
519
+ const layoutTemplate = await layoutService.createTemplateLayout({
520
+ ...testTemplate,
521
+ name: 'subject-layout',
522
+ content: '<!DOCTYPE html>\n<html>\n<head>\n <title>{{ title }}</title>\n</head>\n<body>\n {{ content }}\n</body>\n</html>'
523
+ });
524
+
525
+ // Create content template with layout
526
+ const contentTemplate = await service.createTemplate({
527
+ ...testTemplate,
528
+ name: 'subject-content',
529
+ content: 'Hello World',
530
+ templateLayoutName: 'subject-layout',
531
+ subject: 'Email from {{ sender }}'
532
+ });
533
+
534
+ const result = await service.render({
535
+ name: 'subject-content',
536
+ scope: 'system',
537
+ context: {
538
+ title: 'Test Page',
539
+ subject: 'Important Message',
540
+ sender: 'John Doe'
541
+ }
542
+ });
543
+
544
+ expect(result.subject).toBe('Email from John Doe');
545
+ expect(result.content).toMatch(/<!DOCTYPE html>/);
546
+ expect(result.content).toMatch(/<title>Test Page<\/title>/);
547
+ expect(result.content).toMatch(/<body>\s*Hello World\s*<\/body>/);
548
+ });
549
+ });
550
+
551
+ describe('Template with Multiple Locales', () => {
552
+ it('should render template with correct locale', async () => {
553
+ // Create system template with English content
554
+ const systemTemplate = await service.createTemplate({
555
+ ...testTemplate,
556
+ name: 'locale-template',
557
+ content: 'Hello {{name}}!',
558
+ locale: 'en'
559
+ });
560
+
561
+ // Create Spanish template
562
+ const spanishTemplate = await service.createTemplate({
563
+ ...testTemplate,
564
+ name: 'locale-template',
565
+ content: '¡Hola {{name}}!',
566
+ locale: 'es'
567
+ });
568
+
569
+ // Create French template
570
+ const frenchTemplate = await service.createTemplate({
571
+ ...testTemplate,
572
+ name: 'locale-template',
573
+ content: 'Bonjour {{name}}!',
574
+ locale: 'fr'
575
+ });
576
+
577
+ // Test English rendering
578
+ const englishResult = await service.render({
579
+ name: 'locale-template',
580
+ scope: 'system',
581
+ locale: 'en',
582
+ context: { name: 'John' }
583
+ });
584
+ expect(englishResult.content).toBe('Hello John!');
585
+
586
+ // Test Spanish rendering
587
+ const spanishResult = await service.render({
588
+ name: 'locale-template',
589
+ scope: 'system',
590
+ locale: 'es',
591
+ context: { name: 'John' }
592
+ });
593
+ expect(spanishResult.content).toBe('¡Hola John!');
594
+
595
+ // Test French rendering
596
+ const frenchResult = await service.render({
597
+ name: 'locale-template',
598
+ scope: 'system',
599
+ locale: 'fr',
600
+ context: { name: 'John' }
601
+ });
602
+ expect(frenchResult.content).toBe('Bonjour John!');
603
+ });
604
+
605
+ it('should fallback to system locale when requested locale not found', async () => {
606
+ // Create system template with English content
607
+ const systemTemplate = await service.createTemplate({
608
+ ...testTemplate,
609
+ name: 'fallback-template',
610
+ content: 'Hello {{name}}!',
611
+ locale: 'en'
612
+ });
613
+
614
+ // Create Spanish template
615
+ const spanishTemplate = await service.createTemplate({
616
+ ...testTemplate,
617
+ name: 'fallback-template',
618
+ content: '¡Hola {{name}}!',
619
+ locale: 'es'
620
+ });
621
+
622
+ // Test with non-existent locale (should fallback to English)
623
+ const result = await service.render({
624
+ name: 'fallback-template',
625
+ scope: 'system',
626
+ locale: 'de', // German locale not defined
627
+ context: { name: 'John' }
628
+ });
629
+ expect(result.content).toBe('Hello John!');
630
+ });
631
+
632
+ it('should handle locale-specific overwrites', async () => {
633
+ // Create system template with English content
634
+ const systemTemplate = await service.createTemplate({
635
+ ...testTemplate,
636
+ name: 'locale-overwrite',
637
+ content: 'Hello {{name}}!',
638
+ locale: 'en'
639
+ });
640
+
641
+ // Create Spanish template
642
+ const spanishTemplate = await service.createTemplate({
643
+ ...testTemplate,
644
+ name: 'locale-overwrite',
645
+ content: '¡Hola {{name}}!',
646
+ locale: 'es'
647
+ });
648
+
649
+ // Create custom scope English template
650
+ const customEnglishTemplate = await service.overwriteSystemTemplate(systemTemplate.id, {
651
+ ...testTemplate,
652
+ name: 'locale-overwrite',
653
+ content: 'Custom Hello {{name}}!',
654
+ locale: 'en',
655
+ scope: 'custom',
656
+ scopeId: '123'
657
+ });
658
+
659
+ // Create custom scope Spanish template
660
+ const customSpanishTemplate = await service.overwriteSystemTemplate(spanishTemplate.id, {
661
+ ...testTemplate,
662
+ name: 'locale-overwrite',
663
+ content: 'Custom ¡Hola {{name}}!',
664
+ locale: 'es',
665
+ scope: 'custom',
666
+ scopeId: '123'
667
+ });
668
+
669
+ // Test system English
670
+ const systemEnglishResult = await service.render({
671
+ name: 'locale-overwrite',
672
+ scope: 'system',
673
+ locale: 'en',
674
+ context: { name: 'John' }
675
+ });
676
+ expect(systemEnglishResult.content).toBe('Hello John!');
677
+
678
+ // Test system Spanish
679
+ const systemSpanishResult = await service.render({
680
+ name: 'locale-overwrite',
681
+ scope: 'system',
682
+ locale: 'es',
683
+ context: { name: 'John' }
684
+ });
685
+ expect(systemSpanishResult.content).toBe('¡Hola John!');
686
+
687
+ // Test custom English
688
+ const customEnglishResult = await service.render({
689
+ name: 'locale-overwrite',
690
+ scope: 'custom',
691
+ scopeId: '123',
692
+ locale: 'en',
693
+ context: { name: 'John' }
694
+ });
695
+ expect(customEnglishResult.content).toBe('Custom Hello John!');
696
+
697
+ // Test custom Spanish
698
+ const customSpanishResult = await service.render({
699
+ name: 'locale-overwrite',
700
+ scope: 'custom',
701
+ scopeId: '123',
702
+ locale: 'es',
703
+ context: { name: 'John' }
704
+ });
705
+ expect(customSpanishResult.content).toBe('Custom ¡Hola John!');
706
+ });
707
+
708
+ it('should handle locale-specific layouts', async () => {
709
+ // Create system layout with English content
710
+ const systemLayout = await layoutService.createTemplateLayout({
711
+ ...testTemplate,
712
+ name: 'locale-layout',
713
+ content: '<!DOCTYPE html>\n<html>\n<head>\n <title>{{ title }}</title>\n</head>\n<body>\n <header>{{ header }}</header>\n <main>{{ content }}</main>\n <footer>{{ footer }}</footer>\n</body>\n</html>',
714
+ locale: 'en'
715
+ });
716
+
717
+ // Create Spanish layout
718
+ const spanishLayout = await layoutService.createTemplateLayout({
719
+ ...testTemplate,
720
+ name: 'locale-layout',
721
+ content: '<!DOCTYPE html>\n<html>\n<head>\n <title>{{ title }}</title>\n</head>\n<body>\n <header>{{ header }}</header>\n <main>{{ content }}</main>\n <footer>{{ footer }}</footer>\n</body>\n</html>',
722
+ locale: 'es'
723
+ });
724
+
725
+ // Create content template
726
+ const contentTemplate = await service.createTemplate({
727
+ ...testTemplate,
728
+ name: 'locale-content',
729
+ content: '{{ message }}',
730
+ templateLayoutName: 'locale-layout'
731
+ });
732
+
733
+ // Test English layout
734
+ const englishResult = await service.render({
735
+ name: 'locale-content',
736
+ scope: 'system',
737
+ locale: 'en',
738
+ context: {
739
+ title: 'Welcome',
740
+ header: 'Hello',
741
+ message: 'This is English content',
742
+ footer: '© 2024'
743
+ }
744
+ });
745
+ expect(englishResult.content).toMatch(/<title>Welcome<\/title>/);
746
+ expect(englishResult.content).toMatch(/<header>Hello<\/header>/);
747
+ expect(englishResult.content).toMatch(/<main>This is English content<\/main>/);
748
+ expect(englishResult.content).toMatch(/<footer>© 2024<\/footer>/);
749
+
750
+ // Test Spanish layout
751
+ const spanishResult = await service.render({
752
+ name: 'locale-content',
753
+ scope: 'system',
754
+ locale: 'es',
755
+ context: {
756
+ title: 'Bienvenido',
757
+ header: 'Hola',
758
+ message: 'Este es contenido en español',
759
+ footer: '© 2024'
760
+ }
761
+ });
762
+ expect(spanishResult.content).toMatch(/<title>Bienvenido<\/title>/);
763
+ expect(spanishResult.content).toMatch(/<header>Hola<\/header>/);
764
+ expect(spanishResult.content).toMatch(/<main>Este es contenido en español<\/main>/);
765
+ expect(spanishResult.content).toMatch(/<footer>© 2024<\/footer>/);
766
+ });
767
+ });
768
+
769
+ describe('Template Resolution Order', () => {
770
+ it('should find scoped template with matching locale first', async () => {
771
+ // Create system template
772
+ const systemTemplate = await service.createTemplate({
773
+ ...testTemplate,
774
+ name: 'test-template',
775
+ content: 'System Template',
776
+ locale: 'en'
777
+ });
778
+
779
+ // Create scoped template with English locale
780
+ await service.overwriteSystemTemplate(systemTemplate.id, {
781
+ ...testTemplate,
782
+ content: 'Scoped English Template',
783
+ locale: 'en',
784
+ scope: 'custom',
785
+ scopeId: '123'
786
+ });
787
+
788
+ const result = await service.findTemplate(
789
+ 'test-template',
790
+ 'custom',
791
+ '123',
792
+ 'en'
793
+ );
794
+
795
+ expect(result.content).toBe('Scoped English Template');
796
+ });
797
+
798
+ it('should fallback to system template with locale when no scoped template exists', async () => {
799
+ // Create system template with English locale
800
+ const systemTemplate = await service.createTemplate({
801
+ ...testTemplate,
802
+ name: 'test-template',
803
+ content: 'System English Template',
804
+ locale: 'en'
805
+ });
806
+
807
+ const result = await service.findTemplate(
808
+ 'test-template',
809
+ 'custom',
810
+ '123',
811
+ 'en'
812
+ );
813
+
814
+ expect(result.content).toBe('System English Template');
815
+ });
816
+
817
+ it('should follow same resolution order for render function', async () => {
818
+ // Create system template with English locale
819
+ const systemTemplateEn = await service.createTemplate({
820
+ ...testTemplate,
821
+ name: 'test-template',
822
+ content: 'System English Template',
823
+ locale: 'en'
824
+ });
825
+
826
+ // Create scoped template with English locale
827
+ await service.overwriteSystemTemplate(systemTemplateEn.id, {
828
+ ...testTemplate,
829
+ name: 'test-template',
830
+ content: 'Scoped English Template',
831
+ locale: 'en',
832
+ scope: 'custom',
833
+ scopeId: '123'
834
+ });
835
+
836
+ // Test all resolution steps with render function
837
+ const results = await Promise.all([
838
+ // 1. Scoped template with locale
839
+ service.render({
840
+ name: 'test-template',
841
+ scope: 'custom',
842
+ scopeId: '123',
843
+ locale: 'en'
844
+ }),
845
+
846
+ // 2. System template with locale (after deleting scoped templates)
847
+ (async () => {
848
+ await templateRepository.delete({ scope: 'custom', scopeId: '123' });
849
+ return service.render({
850
+ name: 'test-template',
851
+ scope: 'custom',
852
+ scopeId: '123',
853
+ locale: 'en'
854
+ });
855
+ })()
856
+ ]);
857
+
858
+ expect(results[0].content).toBe('Scoped English Template');
859
+ expect(results[1].content).toBe('System English Template');
860
+ });
861
+ });
862
+ });