@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,476 @@
1
+ import { Injectable, ForbiddenException, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
2
+ import { InjectRepository } from '@nestjs/typeorm';
3
+ import { IsNull, Repository, Not, In, Equal } from 'typeorm';
4
+ import { NestDynamicTemplate } from '../entities/template.entity';
5
+ import { TemplateEngineEnum, TemplateLanguageEnum, TemplateTypeEnum } from '../interfaces/template.types';
6
+ import { RenderTemplateDto, RenderTemplateOutputDTO } from '../dto/render-template.dto';
7
+ import { CreateTemplateDto } from '../dto/create-template.dto';
8
+ import { TemplateFilterDto } from '../dto/template-filter.dto';
9
+ import { TemplateLayoutService } from './template-layout.service';
10
+ import { TemplateEngineRegistryService } from './template-engine.registry';
11
+ import { omit } from 'lodash';
12
+ import { RenderContentTemplateDto } from '../dto/render-content-template.dto';
13
+ import { NestDynamicTemplateLayout } from '../entities/template-layout.entity';
14
+ import {
15
+ TemplateRenderError,
16
+ TemplateEngineError,
17
+ TemplateLanguageError,
18
+ TemplateLayoutError,
19
+ TemplateContentError
20
+ } from '../errors/template.errors';
21
+
22
+ @Injectable()
23
+ export class TemplateService {
24
+ constructor(
25
+ @InjectRepository(NestDynamicTemplate)
26
+ private readonly templateRepository: Repository<NestDynamicTemplate>,
27
+ private readonly engineRegistry: TemplateEngineRegistryService,
28
+ private readonly templateLayoutService: TemplateLayoutService,
29
+ ) { }
30
+
31
+ async render(renderDto: RenderTemplateDto): Promise<RenderTemplateOutputDTO> {
32
+ const { name, scope, scopeId, locale, context } = renderDto;
33
+
34
+ try {
35
+ // Find template with fallback
36
+ if (!name) {
37
+ throw new BadRequestException('Template name is required');
38
+ }
39
+ const template = await this.findTemplate(name, scope || 'system', scopeId, locale);
40
+ if (!template) {
41
+ throw new NotFoundException(`Template not found: ${name} in scope ${scope || 'system'}`);
42
+ }
43
+
44
+ let content = template.content;
45
+ let subject = template.subject;
46
+
47
+ // Render subject by template engine
48
+ if (template.subject && template.engine) {
49
+ try {
50
+ const subjectEngine = this.engineRegistry.getTemplateEngine(template.engine);
51
+ subject = await subjectEngine.render(template.subject, context || {});
52
+ } catch (error) {
53
+ throw new TemplateEngineError(template.engine, error as Error);
54
+ }
55
+ }
56
+
57
+ // Render content by template engine
58
+ if (template.engine) {
59
+ try {
60
+ content = await this.renderEngine(template.engine, content, context || {});
61
+ } catch (error) {
62
+ throw new TemplateEngineError(template.engine, error as Error);
63
+ }
64
+ }
65
+
66
+ // If template has layout, apply it
67
+ let layout: any;
68
+ if (template.templateLayoutName) {
69
+ try {
70
+ layout = await this.templateLayoutService.render({
71
+ name: template.templateLayoutName,
72
+ scope: scope || 'system',
73
+ scopeId,
74
+ locale,
75
+ context: {
76
+ ...(context || {}),
77
+ content
78
+ }
79
+ });
80
+ content = layout.content;
81
+ } catch (error) {
82
+ throw new TemplateLayoutError(template.templateLayoutName, error as Error);
83
+ }
84
+ }
85
+
86
+ // If template has language format, process with language engine
87
+ if (!layout && template.language) {
88
+ try {
89
+ content = await this.renderLanguage(template.language, content, context || {});
90
+ } catch (error) {
91
+ throw new TemplateLanguageError(template.language, error as Error);
92
+ }
93
+ }
94
+
95
+ return {
96
+ subject: subject || '',
97
+ content
98
+ };
99
+ } catch (error) {
100
+ // Re-throw known template errors
101
+ if (error instanceof TemplateEngineError ||
102
+ error instanceof TemplateLanguageError ||
103
+ error instanceof TemplateLayoutError ||
104
+ error instanceof NotFoundException) {
105
+ throw error;
106
+ }
107
+
108
+ // Wrap unknown errors
109
+ throw new TemplateRenderError('template rendering', error as Error, name);
110
+ }
111
+ }
112
+
113
+ async renderContent(input: RenderContentTemplateDto): Promise<string> {
114
+ const { content, language, engine, context, templateLayoutId } = input;
115
+
116
+ try {
117
+ if (!content) {
118
+ throw new BadRequestException('Content is required for rendering');
119
+ }
120
+
121
+ let renderContent = content;
122
+
123
+ // Step 1: Render template variables first
124
+ try {
125
+ renderContent = await this.renderEngine(engine || TemplateEngineEnum.NUNJUCKS, renderContent, context || {});
126
+ } catch (error) {
127
+ throw new TemplateEngineError(engine || TemplateEngineEnum.NUNJUCKS, error as Error);
128
+ }
129
+
130
+ // Step 2: Handle MJML with layouts intelligently
131
+ let templateLayout: NestDynamicTemplateLayout | null = null;
132
+ if (templateLayoutId) {
133
+ try {
134
+ templateLayout = await this.templateLayoutService.getTemplateLayoutById(templateLayoutId);
135
+ if (!templateLayout) {
136
+ throw new NotFoundException(`Template layout not found with ID: ${templateLayoutId}`);
137
+ }
138
+ } catch (error) {
139
+ if (error instanceof NotFoundException) {
140
+ throw error;
141
+ }
142
+ throw new TemplateContentError('template layout retrieval', error as Error);
143
+ }
144
+
145
+ if (templateLayout) {
146
+ try {
147
+ // Step 3: Render the layout content
148
+ renderContent = await this.templateLayoutService.renderContent({
149
+ content: templateLayout.content,
150
+ language: language,
151
+ engine: engine,
152
+ context: {
153
+ ...(context || {}),
154
+ content: renderContent
155
+ }
156
+ });
157
+ } catch (error) {
158
+ throw new TemplateLayoutError(templateLayout.name, error as Error);
159
+ }
160
+ }
161
+ }
162
+
163
+ // Step 4: Render the content with the language engine
164
+ if ((!templateLayoutId || !templateLayout) && language) {
165
+ try {
166
+ renderContent = await this.renderLanguage(language, renderContent, context || {});
167
+ } catch (error) {
168
+ throw new TemplateLanguageError(language, error as Error);
169
+ }
170
+ }
171
+
172
+ return renderContent;
173
+ } catch (error) {
174
+ // Re-throw known template errors
175
+ if (error instanceof TemplateEngineError ||
176
+ error instanceof TemplateLanguageError ||
177
+ error instanceof TemplateLayoutError ||
178
+ error instanceof TemplateContentError ||
179
+ error instanceof NotFoundException ||
180
+ error instanceof BadRequestException) {
181
+ throw error;
182
+ }
183
+
184
+ // Wrap unknown errors
185
+ throw new TemplateRenderError('content rendering', error as Error);
186
+ }
187
+ }
188
+
189
+ async renderLanguage(language: TemplateLanguageEnum, content: string, context: Record<string, any>): Promise<string> {
190
+ try {
191
+ if (!content) {
192
+ throw new BadRequestException('Content is required for language rendering');
193
+ }
194
+
195
+ const languageEngine = this.engineRegistry.getLanguageEngine(language);
196
+ if (!languageEngine) {
197
+ throw new BadRequestException(`Language engine not found for: ${language}`);
198
+ }
199
+
200
+ return await languageEngine.render(content, context || {});
201
+ } catch (error) {
202
+ if (error instanceof BadRequestException) {
203
+ throw error;
204
+ }
205
+ throw new TemplateLanguageError(language, error as Error);
206
+ }
207
+ }
208
+
209
+ async renderEngine(engine: TemplateEngineEnum, content: string, context: Record<string, any>): Promise<string> {
210
+ try {
211
+ if (!content) {
212
+ throw new BadRequestException('Content is required for engine rendering');
213
+ }
214
+
215
+ const templateEngine = this.engineRegistry.getTemplateEngine(engine);
216
+ if (!templateEngine) {
217
+ throw new BadRequestException(`Template engine not found for: ${engine}`);
218
+ }
219
+
220
+ return await templateEngine.render(content, context || {});
221
+ } catch (error) {
222
+ if (error instanceof BadRequestException) {
223
+ throw error;
224
+ }
225
+ throw new TemplateEngineError(engine, error as Error);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Get all templates, with scoped templates taking precedence over system templates
231
+ */
232
+ async getTemplates(filter: TemplateFilterDto = {}): Promise<NestDynamicTemplate[]> {
233
+ const {
234
+ scope,
235
+ scopeId,
236
+ type,
237
+ locale,
238
+ excludeNames = [],
239
+ } = filter;
240
+
241
+
242
+ // Build the where clause
243
+ const where: any = {};
244
+ if (type) where.type = type;
245
+ if (locale) where.locale = locale;
246
+ if (excludeNames.length > 0) where.name = Not(In(excludeNames));
247
+
248
+ const systemTemplates = await this.templateRepository.find({
249
+ where: {
250
+ ...where,
251
+ scope: 'system',
252
+ scopeId: IsNull(),
253
+ },
254
+ });
255
+
256
+ if (scope === 'system') {
257
+ return systemTemplates;
258
+ }
259
+
260
+ // First get all templates matching the filters
261
+ const templates = await this.templateRepository.find({
262
+ where: {
263
+ ...where,
264
+ scope: Equal(scope),
265
+ scopeId: scopeId as any,
266
+ },
267
+ order: {
268
+ createdAt: 'DESC',
269
+ },
270
+ });
271
+
272
+ // Create a map to store unique templates by name+type
273
+ const templateMap = new Map<string, NestDynamicTemplate>();
274
+
275
+ for (const template of systemTemplates) {
276
+ const key = `${template.type}/${template.name}/${template.locale}`;
277
+ templateMap.set(key, template);
278
+ }
279
+
280
+ for (const template of templates) {
281
+ const key = `${template.type}/${template.name}/${template.locale}`;
282
+ templateMap.set(key, template);
283
+ }
284
+
285
+ // Convert map values back to array
286
+ return Array.from(templateMap.values());
287
+ }
288
+
289
+ async getTemplateById(id: string): Promise<NestDynamicTemplate | null> {
290
+ return this.templateRepository.findOne({
291
+ where: { id },
292
+ });
293
+ }
294
+
295
+ async findTemplate(
296
+ name: string,
297
+ scope?: string,
298
+ scopeId?: string,
299
+ locale?: string
300
+ ): Promise<NestDynamicTemplate | null> {
301
+ // Try to find template in the following order:
302
+ // 1. Scoped template with locale
303
+ // 2. Scoped template without locale
304
+ // 3. System template with locale
305
+ // 4. System template without locale
306
+
307
+ const locales = (locale ? [locale, 'en'] : ['en']).filter(Boolean);
308
+
309
+ // First try to find in the specified scope
310
+ for (const currentLocale of locales) {
311
+ const template = await this.templateRepository.findOne({
312
+ where: {
313
+ name,
314
+ scope,
315
+ scopeId: scope === 'system' ? IsNull() : Equal(scopeId),
316
+ locale: currentLocale,
317
+ }
318
+ });
319
+
320
+ if (template) {
321
+ return template;
322
+ }
323
+ }
324
+
325
+ // If not found and not already in system scope, try system scope
326
+ if (scope !== 'system') {
327
+ for (const currentLocale of locales) {
328
+ const template = await this.templateRepository.findOne({
329
+ where: {
330
+ name,
331
+ scope: 'system',
332
+ scopeId: IsNull(),
333
+ locale: currentLocale,
334
+ }
335
+ });
336
+
337
+ if (template) {
338
+ return template;
339
+ }
340
+ }
341
+ }
342
+
343
+ return null;
344
+ }
345
+
346
+ /**
347
+ * Create a system template. Only system templates can be created directly.
348
+ */
349
+ async createTemplate(data: CreateTemplateDto): Promise<NestDynamicTemplate> {
350
+ // Ensure this is a system template
351
+ if (data.scope !== 'system') {
352
+ throw new ForbiddenException('Only system templates can be created directly');
353
+ }
354
+
355
+ // Check if template already exists
356
+ const existingTemplate = await this.templateRepository.findOne({
357
+ where: {
358
+ name: data.name,
359
+ scope: 'system',
360
+ scopeId: IsNull(),
361
+ locale: data.locale,
362
+ },
363
+ });
364
+
365
+ if (existingTemplate) {
366
+ throw new ConflictException(`System template already exists`);
367
+ }
368
+
369
+ const template = this.templateRepository.create({
370
+ ...data,
371
+ scopeId: undefined, // Ensure system templates have no scopeId
372
+ });
373
+
374
+ return this.templateRepository.save(template);
375
+ }
376
+
377
+ async overwriteSystemTemplate(templateId: string, updates: Partial<CreateTemplateDto>): Promise<NestDynamicTemplate> {
378
+ let template = await this.templateRepository.findOne({
379
+ where: { id: templateId },
380
+ });
381
+
382
+ if (!template) {
383
+ throw new NotFoundException(`Template not found: ${templateId}`);
384
+ }
385
+
386
+ if (template.scope === 'system') {
387
+ if (!updates.scope) {
388
+ throw new BadRequestException('Scope is required when overwriting system template');
389
+ }
390
+
391
+ // Check if template already exists in target scope
392
+ const existingTemplate = await this.templateRepository.findOne({
393
+ where: {
394
+ name: template.name,
395
+ locale: template.locale,
396
+ scope: updates.scope,
397
+ scopeId: updates.scopeId as any,
398
+ },
399
+ });
400
+
401
+ if (existingTemplate) {
402
+ // Update existing template in target scope
403
+ template = existingTemplate;
404
+ } else {
405
+ // Create new template in target scope
406
+ const newTemplate = this.templateRepository.create({
407
+ ...template,
408
+ id: undefined,
409
+ createdAt: undefined,
410
+ updatedAt: undefined,
411
+ scope: updates.scope,
412
+ scopeId: updates.scopeId,
413
+ });
414
+ template = newTemplate;
415
+ }
416
+ }
417
+ updates = omit(updates, ['name', 'id', 'createdAt', 'updatedAt']);
418
+
419
+ template = this.templateRepository.merge(template, updates);
420
+ await this.templateRepository.save(template);
421
+ return template;
422
+ }
423
+
424
+
425
+ /**
426
+ * Update a template
427
+ */
428
+ async updateTemplate(
429
+ id: string,
430
+ updates: Partial<CreateTemplateDto>,
431
+ canUpdateSystemTemplate: boolean = false,
432
+ ): Promise<NestDynamicTemplate> {
433
+ // Find the template
434
+ let template = await this.templateRepository.findOne({
435
+ where: { id },
436
+ });
437
+
438
+ if (!template) {
439
+ throw new NotFoundException(`Template not found: ${id}`);
440
+ }
441
+
442
+ // If it's a system template and we can't update it, try to overwrite it
443
+ if (template.scope === 'system' && !canUpdateSystemTemplate) {
444
+ if (updates.scope) {
445
+ // Otherwise, allow overwriting to custom scope
446
+ return this.overwriteSystemTemplate(id, updates);
447
+ } else {
448
+ throw new ForbiddenException('Cannot update system templates');
449
+ }
450
+ }
451
+
452
+ // For regular updates
453
+ template = this.templateRepository.merge(template, updates);
454
+ return this.templateRepository.save(template);
455
+ }
456
+
457
+ /**
458
+ * Delete a scoped template
459
+ */
460
+ async deleteTemplate(id: string, canDeleteSystemTemplate: boolean = false): Promise<void> {
461
+ const template = await this.templateRepository.findOne({
462
+ where: { id },
463
+ });
464
+
465
+ if (!template) {
466
+ throw new Error(`Template not found: ${id}`);
467
+ }
468
+
469
+ // Prevent deleting system templates
470
+ if (template.scope === 'system' && !canDeleteSystemTemplate) {
471
+ throw new ForbiddenException('Cannot delete system templates');
472
+ }
473
+
474
+ await this.templateRepository.remove(template);
475
+ }
476
+ }
@@ -1,11 +1,9 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.engineFilters = void 0;
4
- exports.engineFilters = {
5
- formatDate: (date, format) => {
1
+ export const engineFilters = {
2
+ formatDate: (date: Date, format: string) => {
6
3
  const year = date.getFullYear();
7
4
  const month = String(date.getMonth() + 1).padStart(2, '0');
8
5
  const day = String(date.getDate()).padStart(2, '0');
6
+
9
7
  switch (format) {
10
8
  case 'YYYY-MM-DD':
11
9
  return `${year}-${month}-${day}`;
@@ -21,7 +19,7 @@ exports.engineFilters = {
21
19
  return `${year}-${month}-${day}`;
22
20
  }
23
21
  },
24
- formatCurrency: (amount, currency) => {
22
+ formatCurrency: (amount: number, currency: string) => {
25
23
  const formatter = new Intl.NumberFormat('en-US', {
26
24
  style: 'currency',
27
25
  currency: currency,
@@ -30,5 +28,4 @@ exports.engineFilters = {
30
28
  });
31
29
  return formatter.format(amount);
32
30
  }
33
- };
34
- //# sourceMappingURL=helpers.js.map
31
+ }
@@ -0,0 +1,157 @@
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 { engineFilters } from './helpers';
8
+
9
+ describe('Nunjucks Engine', () => {
10
+ let service: TemplateService;
11
+ let dataSource: DataSource;
12
+ let templateRepository: Repository<NestDynamicTemplate>;
13
+
14
+ const testTemplate: CreateTemplateDto = {
15
+ name: 'test-template',
16
+ displayName: 'Test Template',
17
+ content: 'Hello {{name}}!',
18
+ type: TemplateTypeEnum.EMAIL,
19
+ engine: TemplateEngineEnum.NUNJUCKS,
20
+ language: TemplateLanguageEnum.HTML,
21
+ scope: 'system',
22
+ scopeId: null,
23
+ locale: 'en',
24
+ isActive: true
25
+ };
26
+
27
+ beforeEach(async () => {
28
+ const moduleRef = await createTestModule({
29
+ enginesOptions: {
30
+ filters: engineFilters
31
+ }
32
+ });
33
+
34
+ service = moduleRef.get<TemplateService>(TemplateService);
35
+ dataSource = moduleRef.get<DataSource>(DataSource);
36
+ templateRepository = dataSource.getRepository(NestDynamicTemplate);
37
+ });
38
+
39
+ afterEach(async () => {
40
+ // Clean up templates
41
+ await templateRepository.delete('1 = 1');
42
+
43
+ // Clean up database after each test
44
+ await dataSource.synchronize(true);
45
+ });
46
+
47
+ afterAll(async () => {
48
+ // Close the connection after all tests
49
+ await dataSource.destroy();
50
+ });
51
+
52
+ describe('Nunjucks Custom Filters', () => {
53
+ it('should format date using custom date filter', async () => {
54
+ // Create template with date filter
55
+ const template = await service.createTemplate({
56
+ ...testTemplate,
57
+ name: 'date-template',
58
+ content: 'Today is {{ date | formatDate("YYYY-MM-DD") }}',
59
+ });
60
+
61
+ const today = new Date();
62
+ const expectedDate = today.toISOString().split('T')[0]; // YYYY-MM-DD format
63
+
64
+ const result = await service.render({
65
+ name: 'date-template',
66
+ scope: 'system',
67
+ context: {
68
+ date: today
69
+ }
70
+ });
71
+
72
+ expect(result.content).toBe(`Today is ${expectedDate}`);
73
+ });
74
+
75
+ it('should format currency using custom currency filter', async () => {
76
+ // Create template with currency filter
77
+ const template = await service.createTemplate({
78
+ ...testTemplate,
79
+ name: 'currency-template',
80
+ content: 'Price: {{ amount | formatCurrency("USD") }}',
81
+ });
82
+
83
+ const result = await service.render({
84
+ name: 'currency-template',
85
+ scope: 'system',
86
+ context: {
87
+ amount: 1234.56
88
+ }
89
+ });
90
+
91
+ expect(result.content).toBe('Price: $1,234.56');
92
+ });
93
+
94
+ it('should handle multiple filters in the same template', async () => {
95
+ // Create template with multiple filters
96
+ const template = await service.createTemplate({
97
+ ...testTemplate,
98
+ name: 'multi-filter-template',
99
+ content: 'Order placed on {{ date | formatDate("YYYY-MM-DD") }} for {{ amount | formatCurrency("USD") }}',
100
+ });
101
+
102
+ const today = new Date();
103
+ const expectedDate = today.toISOString().split('T')[0];
104
+
105
+ const result = await service.render({
106
+ name: 'multi-filter-template',
107
+ scope: 'system',
108
+ context: {
109
+ date: today,
110
+ amount: 1234.56
111
+ }
112
+ });
113
+
114
+ expect(result.content).toBe(`Order placed on ${expectedDate} for $1,234.56`);
115
+ });
116
+
117
+ it('should handle different currency formats', async () => {
118
+ // Create template with different currency formats
119
+ const template = await service.createTemplate({
120
+ ...testTemplate,
121
+ name: 'currency-formats-template',
122
+ content: 'USD: {{ usd | formatCurrency("USD") }}, EUR: {{ eur | formatCurrency("EUR") }}',
123
+ });
124
+
125
+ const result = await service.render({
126
+ name: 'currency-formats-template',
127
+ scope: 'system',
128
+ context: {
129
+ usd: 1234.56,
130
+ eur: 1234.56,
131
+ }
132
+ });
133
+
134
+ expect(result.content).toBe('USD: $1,234.56, EUR: €1,234.56');
135
+ });
136
+
137
+ it('should handle different date formats', async () => {
138
+ // Create template with different date formats
139
+ const template = await service.createTemplate({
140
+ ...testTemplate,
141
+ name: 'date-formats-template',
142
+ content: 'Short: {{ date | formatDate("MM/DD/YY") }}, Long: {{ date | formatDate("MMMM D, YYYY") }}, ISO: {{ date | formatDate("YYYY-MM-DD") }}',
143
+ });
144
+
145
+ const date = new Date('2024-03-15');
146
+ const result = await service.render({
147
+ name: 'date-formats-template',
148
+ scope: 'system',
149
+ context: {
150
+ date: date
151
+ }
152
+ });
153
+
154
+ expect(result.content).toBe('Short: 03/15/24, Long: March 15, 2024, ISO: 2024-03-15');
155
+ });
156
+ });
157
+ });