@ackplus/nest-dynamic-templates 1.1.2 → 1.1.6

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +25 -0
  3. package/src/lib/constant.js +5 -0
  4. package/src/lib/dto/create-template-layout.dto.js +86 -0
  5. package/src/lib/dto/create-template.dto.js +103 -0
  6. package/src/lib/dto/render-content-template-layout.dto.js +40 -0
  7. package/src/lib/dto/render-content-template.dto.js +46 -0
  8. package/src/lib/dto/render-template-layout.dto.js +66 -0
  9. package/src/lib/dto/render-template.dto.js +90 -0
  10. package/src/lib/dto/template-filter.dto.js +61 -0
  11. package/src/lib/dto/template-layout-filter.dto.js +60 -0
  12. package/src/lib/engines/language/html.engine.js +80 -0
  13. package/src/lib/engines/language/index.js +11 -0
  14. package/src/lib/engines/language/markdown.engine.js +39 -0
  15. package/src/lib/engines/language/mjml.engine.js +75 -0
  16. package/src/lib/engines/language/text.engine.js +15 -0
  17. package/src/lib/engines/language-engine.js +6 -0
  18. package/src/lib/engines/template/ejs.engine.js +65 -0
  19. package/src/lib/engines/template/handlebars.engine.js +66 -0
  20. package/src/lib/engines/template/index.js +11 -0
  21. package/src/lib/engines/template/nunjucks.engine.js +64 -0
  22. package/src/lib/engines/template/pug.engine.js +74 -0
  23. package/src/lib/engines/template-engine.js +6 -0
  24. package/src/lib/entities/template-layout.entity.js +120 -0
  25. package/src/lib/entities/template.entity.js +127 -0
  26. package/src/lib/errors/template.errors.js +75 -0
  27. package/src/lib/interfaces/module-config.interface.js +2 -0
  28. package/src/lib/interfaces/template.types.js +24 -0
  29. package/src/lib/nest-dynamic-templates.module.js +131 -0
  30. package/src/lib/services/template-config.service.js +101 -0
  31. package/src/lib/services/template-engine.registry.js +93 -0
  32. package/src/lib/services/template-layout.service.js +343 -0
  33. package/src/lib/services/template.service.js +414 -0
  34. package/src/test/helpers.js +33 -0
  35. package/src/test/test-database.config.js +23 -0
  36. package/src/test/test.setup.js +30 -0
@@ -0,0 +1,414 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TemplateService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const typeorm_1 = require("@nestjs/typeorm");
7
+ const typeorm_2 = require("typeorm");
8
+ const template_entity_1 = require("../entities/template.entity");
9
+ const template_types_1 = require("../interfaces/template.types");
10
+ const template_layout_service_1 = require("./template-layout.service");
11
+ const template_engine_registry_1 = require("./template-engine.registry");
12
+ const lodash_1 = require("lodash");
13
+ const template_errors_1 = require("../errors/template.errors");
14
+ let TemplateService = class TemplateService {
15
+ constructor(templateRepository, engineRegistry, templateLayoutService) {
16
+ this.templateRepository = templateRepository;
17
+ this.engineRegistry = engineRegistry;
18
+ this.templateLayoutService = templateLayoutService;
19
+ }
20
+ async render(renderDto) {
21
+ const { name, scope, scopeId, locale, context } = renderDto;
22
+ try {
23
+ // Find template with fallback
24
+ if (!name) {
25
+ throw new common_1.BadRequestException('Template name is required');
26
+ }
27
+ const template = await this.findTemplate(name, scope || 'system', scopeId, locale);
28
+ if (!template) {
29
+ throw new common_1.NotFoundException(`Template not found: ${name} in scope ${scope || 'system'}`);
30
+ }
31
+ let content = template.content;
32
+ let subject = template.subject;
33
+ // Render subject by template engine
34
+ if (template.subject && template.engine) {
35
+ try {
36
+ const subjectEngine = this.engineRegistry.getTemplateEngine(template.engine);
37
+ subject = await subjectEngine.render(template.subject, context || {});
38
+ }
39
+ catch (error) {
40
+ throw new template_errors_1.TemplateEngineError(template.engine, error);
41
+ }
42
+ }
43
+ // Render content by template engine
44
+ if (template.engine) {
45
+ try {
46
+ content = await this.renderEngine(template.engine, content, context || {});
47
+ }
48
+ catch (error) {
49
+ throw new template_errors_1.TemplateEngineError(template.engine, error);
50
+ }
51
+ }
52
+ // If template has layout, apply it
53
+ let layout;
54
+ if (template.templateLayoutName) {
55
+ try {
56
+ layout = await this.templateLayoutService.render({
57
+ name: template.templateLayoutName,
58
+ scope: scope || 'system',
59
+ scopeId,
60
+ locale,
61
+ context: {
62
+ ...(context || {}),
63
+ content
64
+ }
65
+ });
66
+ content = layout.content;
67
+ }
68
+ catch (error) {
69
+ throw new template_errors_1.TemplateLayoutError(template.templateLayoutName, error);
70
+ }
71
+ }
72
+ // If template has language format, process with language engine
73
+ if (!layout && template.language) {
74
+ try {
75
+ content = await this.renderLanguage(template.language, content, context || {});
76
+ }
77
+ catch (error) {
78
+ throw new template_errors_1.TemplateLanguageError(template.language, error);
79
+ }
80
+ }
81
+ return {
82
+ subject: subject || '',
83
+ content
84
+ };
85
+ }
86
+ catch (error) {
87
+ // Re-throw known template errors
88
+ if (error instanceof template_errors_1.TemplateEngineError ||
89
+ error instanceof template_errors_1.TemplateLanguageError ||
90
+ error instanceof template_errors_1.TemplateLayoutError ||
91
+ error instanceof common_1.NotFoundException) {
92
+ throw error;
93
+ }
94
+ // Wrap unknown errors
95
+ throw new template_errors_1.TemplateRenderError('template rendering', error, name);
96
+ }
97
+ }
98
+ async renderContent(input) {
99
+ const { content, language, engine, context, templateLayoutId } = input;
100
+ try {
101
+ if (!content) {
102
+ throw new common_1.BadRequestException('Content is required for rendering');
103
+ }
104
+ let renderContent = content;
105
+ // Step 1: Render template variables first
106
+ try {
107
+ renderContent = await this.renderEngine(engine || template_types_1.TemplateEngineEnum.NUNJUCKS, renderContent, context || {});
108
+ }
109
+ catch (error) {
110
+ throw new template_errors_1.TemplateEngineError(engine || template_types_1.TemplateEngineEnum.NUNJUCKS, error);
111
+ }
112
+ // Step 2: Handle MJML with layouts intelligently
113
+ let templateLayout = null;
114
+ if (templateLayoutId) {
115
+ try {
116
+ templateLayout = await this.templateLayoutService.getTemplateLayoutById(templateLayoutId);
117
+ if (!templateLayout) {
118
+ throw new common_1.NotFoundException(`Template layout not found with ID: ${templateLayoutId}`);
119
+ }
120
+ }
121
+ catch (error) {
122
+ if (error instanceof common_1.NotFoundException) {
123
+ throw error;
124
+ }
125
+ throw new template_errors_1.TemplateContentError('template layout retrieval', error);
126
+ }
127
+ if (templateLayout) {
128
+ try {
129
+ // Step 3: Render the layout content
130
+ renderContent = await this.templateLayoutService.renderContent({
131
+ content: templateLayout.content,
132
+ language: language,
133
+ engine: engine,
134
+ context: {
135
+ ...(context || {}),
136
+ content: renderContent
137
+ }
138
+ });
139
+ }
140
+ catch (error) {
141
+ throw new template_errors_1.TemplateLayoutError(templateLayout.name, error);
142
+ }
143
+ }
144
+ }
145
+ // Step 4: Render the content with the language engine
146
+ if ((!templateLayoutId || !templateLayout) && language) {
147
+ try {
148
+ renderContent = await this.renderLanguage(language, renderContent, context || {});
149
+ }
150
+ catch (error) {
151
+ throw new template_errors_1.TemplateLanguageError(language, error);
152
+ }
153
+ }
154
+ return renderContent;
155
+ }
156
+ catch (error) {
157
+ // Re-throw known template errors
158
+ if (error instanceof template_errors_1.TemplateEngineError ||
159
+ error instanceof template_errors_1.TemplateLanguageError ||
160
+ error instanceof template_errors_1.TemplateLayoutError ||
161
+ error instanceof template_errors_1.TemplateContentError ||
162
+ error instanceof common_1.NotFoundException ||
163
+ error instanceof common_1.BadRequestException) {
164
+ throw error;
165
+ }
166
+ // Wrap unknown errors
167
+ throw new template_errors_1.TemplateRenderError('content rendering', error);
168
+ }
169
+ }
170
+ async renderLanguage(language, content, context) {
171
+ try {
172
+ if (!content) {
173
+ throw new common_1.BadRequestException('Content is required for language rendering');
174
+ }
175
+ const languageEngine = this.engineRegistry.getLanguageEngine(language);
176
+ if (!languageEngine) {
177
+ throw new common_1.BadRequestException(`Language engine not found for: ${language}`);
178
+ }
179
+ return await languageEngine.render(content, context || {});
180
+ }
181
+ catch (error) {
182
+ if (error instanceof common_1.BadRequestException) {
183
+ throw error;
184
+ }
185
+ throw new template_errors_1.TemplateLanguageError(language, error);
186
+ }
187
+ }
188
+ async renderEngine(engine, content, context) {
189
+ try {
190
+ if (!content) {
191
+ throw new common_1.BadRequestException('Content is required for engine rendering');
192
+ }
193
+ const templateEngine = this.engineRegistry.getTemplateEngine(engine);
194
+ if (!templateEngine) {
195
+ throw new common_1.BadRequestException(`Template engine not found for: ${engine}`);
196
+ }
197
+ return await templateEngine.render(content, context || {});
198
+ }
199
+ catch (error) {
200
+ if (error instanceof common_1.BadRequestException) {
201
+ throw error;
202
+ }
203
+ throw new template_errors_1.TemplateEngineError(engine, error);
204
+ }
205
+ }
206
+ /**
207
+ * Get all templates, with scoped templates taking precedence over system templates
208
+ */
209
+ async getTemplates(filter = {}) {
210
+ const { scope, scopeId, type, locale, excludeNames = [], } = filter;
211
+ // Build the where clause
212
+ const where = {};
213
+ if (type)
214
+ where.type = type;
215
+ if (locale)
216
+ where.locale = locale;
217
+ if (excludeNames.length > 0)
218
+ where.name = (0, typeorm_2.Not)((0, typeorm_2.In)(excludeNames));
219
+ const systemTemplates = await this.templateRepository.find({
220
+ where: {
221
+ ...where,
222
+ scope: 'system',
223
+ scopeId: (0, typeorm_2.IsNull)(),
224
+ },
225
+ });
226
+ if (scope === 'system') {
227
+ return systemTemplates;
228
+ }
229
+ // First get all templates matching the filters
230
+ const templates = await this.templateRepository.find({
231
+ where: {
232
+ ...where,
233
+ scope: (0, typeorm_2.Equal)(scope),
234
+ scopeId: scopeId,
235
+ },
236
+ order: {
237
+ createdAt: 'DESC',
238
+ },
239
+ });
240
+ // Create a map to store unique templates by name+type
241
+ const templateMap = new Map();
242
+ for (const template of systemTemplates) {
243
+ const key = `${template.type}/${template.name}/${template.locale}`;
244
+ templateMap.set(key, template);
245
+ }
246
+ for (const template of templates) {
247
+ const key = `${template.type}/${template.name}/${template.locale}`;
248
+ templateMap.set(key, template);
249
+ }
250
+ // Convert map values back to array
251
+ return Array.from(templateMap.values());
252
+ }
253
+ async getTemplateById(id) {
254
+ return this.templateRepository.findOne({
255
+ where: { id },
256
+ });
257
+ }
258
+ async findTemplate(name, scope, scopeId, locale) {
259
+ // Try to find template in the following order:
260
+ // 1. Scoped template with locale
261
+ // 2. Scoped template without locale
262
+ // 3. System template with locale
263
+ // 4. System template without locale
264
+ const locales = (locale ? [locale, 'en'] : ['en']).filter(Boolean);
265
+ // First try to find in the specified scope
266
+ for (const currentLocale of locales) {
267
+ const template = await this.templateRepository.findOne({
268
+ where: {
269
+ name,
270
+ scope,
271
+ scopeId: scope === 'system' ? (0, typeorm_2.IsNull)() : (0, typeorm_2.Equal)(scopeId),
272
+ locale: currentLocale,
273
+ }
274
+ });
275
+ if (template) {
276
+ return template;
277
+ }
278
+ }
279
+ // If not found and not already in system scope, try system scope
280
+ if (scope !== 'system') {
281
+ for (const currentLocale of locales) {
282
+ const template = await this.templateRepository.findOne({
283
+ where: {
284
+ name,
285
+ scope: 'system',
286
+ scopeId: (0, typeorm_2.IsNull)(),
287
+ locale: currentLocale,
288
+ }
289
+ });
290
+ if (template) {
291
+ return template;
292
+ }
293
+ }
294
+ }
295
+ return null;
296
+ }
297
+ /**
298
+ * Create a system template. Only system templates can be created directly.
299
+ */
300
+ async createTemplate(data) {
301
+ // Ensure this is a system template
302
+ if (data.scope !== 'system') {
303
+ throw new common_1.ForbiddenException('Only system templates can be created directly');
304
+ }
305
+ // Check if template already exists
306
+ const existingTemplate = await this.templateRepository.findOne({
307
+ where: {
308
+ name: data.name,
309
+ scope: 'system',
310
+ scopeId: (0, typeorm_2.IsNull)(),
311
+ locale: data.locale,
312
+ },
313
+ });
314
+ if (existingTemplate) {
315
+ throw new common_1.ConflictException(`System template already exists`);
316
+ }
317
+ const template = this.templateRepository.create({
318
+ ...data,
319
+ scopeId: undefined, // Ensure system templates have no scopeId
320
+ });
321
+ return this.templateRepository.save(template);
322
+ }
323
+ async overwriteSystemTemplate(templateId, updates) {
324
+ let template = await this.templateRepository.findOne({
325
+ where: { id: templateId },
326
+ });
327
+ if (!template) {
328
+ throw new common_1.NotFoundException(`Template not found: ${templateId}`);
329
+ }
330
+ if (template.scope === 'system') {
331
+ if (!updates.scope) {
332
+ throw new common_1.BadRequestException('Scope is required when overwriting system template');
333
+ }
334
+ // Check if template already exists in target scope
335
+ const existingTemplate = await this.templateRepository.findOne({
336
+ where: {
337
+ name: template.name,
338
+ locale: template.locale,
339
+ scope: updates.scope,
340
+ scopeId: updates.scopeId,
341
+ },
342
+ });
343
+ if (existingTemplate) {
344
+ // Update existing template in target scope
345
+ template = existingTemplate;
346
+ }
347
+ else {
348
+ // Create new template in target scope
349
+ const newTemplate = this.templateRepository.create({
350
+ ...template,
351
+ id: undefined,
352
+ createdAt: undefined,
353
+ updatedAt: undefined,
354
+ scope: updates.scope,
355
+ scopeId: updates.scopeId,
356
+ });
357
+ template = newTemplate;
358
+ }
359
+ }
360
+ updates = (0, lodash_1.omit)(updates, ['name', 'id', 'createdAt', 'updatedAt']);
361
+ template = this.templateRepository.merge(template, updates);
362
+ await this.templateRepository.save(template);
363
+ return template;
364
+ }
365
+ /**
366
+ * Update a template
367
+ */
368
+ async updateTemplate(id, updates, canUpdateSystemTemplate = false) {
369
+ // Find the template
370
+ let template = await this.templateRepository.findOne({
371
+ where: { id },
372
+ });
373
+ if (!template) {
374
+ throw new common_1.NotFoundException(`Template not found: ${id}`);
375
+ }
376
+ // If it's a system template and we can't update it, try to overwrite it
377
+ if (template.scope === 'system' && !canUpdateSystemTemplate) {
378
+ if (updates.scope) {
379
+ // Otherwise, allow overwriting to custom scope
380
+ return this.overwriteSystemTemplate(id, updates);
381
+ }
382
+ else {
383
+ throw new common_1.ForbiddenException('Cannot update system templates');
384
+ }
385
+ }
386
+ // For regular updates
387
+ template = this.templateRepository.merge(template, updates);
388
+ return this.templateRepository.save(template);
389
+ }
390
+ /**
391
+ * Delete a scoped template
392
+ */
393
+ async deleteTemplate(id, canDeleteSystemTemplate = false) {
394
+ const template = await this.templateRepository.findOne({
395
+ where: { id },
396
+ });
397
+ if (!template) {
398
+ throw new Error(`Template not found: ${id}`);
399
+ }
400
+ // Prevent deleting system templates
401
+ if (template.scope === 'system' && !canDeleteSystemTemplate) {
402
+ throw new common_1.ForbiddenException('Cannot delete system templates');
403
+ }
404
+ await this.templateRepository.remove(template);
405
+ }
406
+ };
407
+ exports.TemplateService = TemplateService;
408
+ exports.TemplateService = TemplateService = tslib_1.__decorate([
409
+ (0, common_1.Injectable)(),
410
+ tslib_1.__param(0, (0, typeorm_1.InjectRepository)(template_entity_1.NestDynamicTemplate)),
411
+ tslib_1.__metadata("design:paramtypes", [typeorm_2.Repository,
412
+ template_engine_registry_1.TemplateEngineRegistryService,
413
+ template_layout_service_1.TemplateLayoutService])
414
+ ], TemplateService);
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.engineFilters = void 0;
4
+ exports.engineFilters = {
5
+ formatDate: (date, format) => {
6
+ const year = date.getFullYear();
7
+ const month = String(date.getMonth() + 1).padStart(2, '0');
8
+ const day = String(date.getDate()).padStart(2, '0');
9
+ switch (format) {
10
+ case 'YYYY-MM-DD':
11
+ return `${year}-${month}-${day}`;
12
+ case 'MM/DD/YY':
13
+ return `${month}/${day}/${String(year).slice(-2)}`;
14
+ case 'MMMM D, YYYY':
15
+ return date.toLocaleDateString('en-US', {
16
+ year: 'numeric',
17
+ month: 'long',
18
+ day: 'numeric'
19
+ });
20
+ default:
21
+ return `${year}-${month}-${day}`;
22
+ }
23
+ },
24
+ formatCurrency: (amount, currency) => {
25
+ const formatter = new Intl.NumberFormat('en-US', {
26
+ style: 'currency',
27
+ currency: currency,
28
+ minimumFractionDigits: 2,
29
+ maximumFractionDigits: 2
30
+ });
31
+ return formatter.format(amount);
32
+ }
33
+ };
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.testRedisConfig = exports.testDatabaseConfig = void 0;
4
+ const template_entity_1 = require("../lib/entities/template.entity");
5
+ const template_layout_entity_1 = require("../lib/entities/template-layout.entity");
6
+ exports.testDatabaseConfig = {
7
+ type: 'sqlite',
8
+ database: ':memory:',
9
+ entities: [template_entity_1.NestDynamicTemplate, template_layout_entity_1.NestDynamicTemplateLayout],
10
+ logging: ['error', 'warn'],
11
+ synchronize: true, // Disable auto-synchronization
12
+ dropSchema: true, // Drop schema before tests
13
+ };
14
+ exports.testRedisConfig = {
15
+ type: 'single',
16
+ options: {
17
+ host: 'localhost',
18
+ port: 6379,
19
+ retryStrategy: (times) => {
20
+ return Math.min(times * 50, 2000);
21
+ },
22
+ },
23
+ };
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTestModule = createTestModule;
4
+ exports.createTestApp = createTestApp;
5
+ const testing_1 = require("@nestjs/testing");
6
+ const typeorm_1 = require("@nestjs/typeorm");
7
+ const test_database_config_1 = require("./test-database.config");
8
+ const template_layout_entity_1 = require("../lib/entities/template-layout.entity");
9
+ const template_entity_1 = require("../lib/entities/template.entity");
10
+ const common_1 = require("@nestjs/common");
11
+ const nest_dynamic_templates_module_1 = require("../lib/nest-dynamic-templates.module");
12
+ async function createTestModule(options = {}) {
13
+ const moduleRef = await testing_1.Test.createTestingModule({
14
+ imports: [
15
+ typeorm_1.TypeOrmModule.forRoot(test_database_config_1.testDatabaseConfig),
16
+ typeorm_1.TypeOrmModule.forFeature([template_entity_1.NestDynamicTemplate, template_layout_entity_1.NestDynamicTemplateLayout]),
17
+ // RedisModule.forRoot(testRedisConfig),
18
+ nest_dynamic_templates_module_1.NestDynamicTemplatesModule.forRoot(options),
19
+ ],
20
+ }).compile();
21
+ return moduleRef;
22
+ }
23
+ async function createTestApp(options = {}) {
24
+ const moduleRef = await createTestModule(options);
25
+ const app = moduleRef.createNestApplication();
26
+ // Enable validation
27
+ app.useGlobalPipes(new common_1.ValidationPipe());
28
+ await app.init();
29
+ return app;
30
+ }