@ackplus/nest-dynamic-templates 1.1.15 → 2.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/MIGRATION.md +97 -0
  3. package/README.md +201 -116
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/config/resolve-config.d.ts +7 -0
  8. package/dist/lib/config/resolve-config.js +50 -0
  9. package/dist/lib/config/resolve-config.js.map +1 -0
  10. package/dist/lib/constant.d.ts +1 -0
  11. package/dist/lib/constant.js +2 -1
  12. package/dist/lib/constant.js.map +1 -1
  13. package/dist/lib/dto/create-template-layout.dto.js +49 -12
  14. package/dist/lib/dto/create-template-layout.dto.js.map +1 -1
  15. package/dist/lib/dto/create-template.dto.js +59 -14
  16. package/dist/lib/dto/create-template.dto.js.map +1 -1
  17. package/dist/lib/dto/render-template.dto.d.ts +3 -6
  18. package/dist/lib/dto/render-template.dto.js +17 -38
  19. package/dist/lib/dto/render-template.dto.js.map +1 -1
  20. package/dist/lib/engines/language/html.engine.d.ts +2 -3
  21. package/dist/lib/engines/language/html.engine.js +4 -34
  22. package/dist/lib/engines/language/html.engine.js.map +1 -1
  23. package/dist/lib/engines/language/markdown.engine.d.ts +4 -2
  24. package/dist/lib/engines/language/markdown.engine.js +20 -20
  25. package/dist/lib/engines/language/markdown.engine.js.map +1 -1
  26. package/dist/lib/engines/language/mjml.engine.d.ts +4 -2
  27. package/dist/lib/engines/language/mjml.engine.js +18 -20
  28. package/dist/lib/engines/language/mjml.engine.js.map +1 -1
  29. package/dist/lib/engines/language/text.engine.d.ts +2 -1
  30. package/dist/lib/engines/language/text.engine.js +2 -1
  31. package/dist/lib/engines/language/text.engine.js.map +1 -1
  32. package/dist/lib/engines/language-engine.d.ts +4 -1
  33. package/dist/lib/engines/language-engine.js +2 -0
  34. package/dist/lib/engines/language-engine.js.map +1 -1
  35. package/dist/lib/engines/load-peer.d.ts +1 -0
  36. package/dist/lib/engines/load-peer.js +21 -0
  37. package/dist/lib/engines/load-peer.js.map +1 -0
  38. package/dist/lib/engines/template/ejs.engine.d.ts +6 -3
  39. package/dist/lib/engines/template/ejs.engine.js +13 -12
  40. package/dist/lib/engines/template/ejs.engine.js.map +1 -1
  41. package/dist/lib/engines/template/handlebars.engine.d.ts +8 -3
  42. package/dist/lib/engines/template/handlebars.engine.js +26 -13
  43. package/dist/lib/engines/template/handlebars.engine.js.map +1 -1
  44. package/dist/lib/engines/template/nunjucks.engine.d.ts +8 -8
  45. package/dist/lib/engines/template/nunjucks.engine.js +26 -33
  46. package/dist/lib/engines/template/nunjucks.engine.js.map +1 -1
  47. package/dist/lib/engines/template/pug.engine.d.ts +7 -5
  48. package/dist/lib/engines/template/pug.engine.js +11 -20
  49. package/dist/lib/engines/template/pug.engine.js.map +1 -1
  50. package/dist/lib/engines/template-engine.d.ts +4 -1
  51. package/dist/lib/engines/template-engine.js +2 -0
  52. package/dist/lib/engines/template-engine.js.map +1 -1
  53. package/dist/lib/entities/template-layout.entity.js +51 -14
  54. package/dist/lib/entities/template-layout.entity.js.map +1 -1
  55. package/dist/lib/entities/template.entity.js +58 -15
  56. package/dist/lib/entities/template.entity.js.map +1 -1
  57. package/dist/lib/errors/diagnose.d.ts +11 -0
  58. package/dist/lib/errors/diagnose.js +162 -0
  59. package/dist/lib/errors/diagnose.js.map +1 -0
  60. package/dist/lib/errors/template.errors.d.ts +77 -13
  61. package/dist/lib/errors/template.errors.js +104 -59
  62. package/dist/lib/errors/template.errors.js.map +1 -1
  63. package/dist/lib/interfaces/module-config.interface.d.ts +29 -18
  64. package/dist/lib/nest-dynamic-templates.module.d.ts +1 -4
  65. package/dist/lib/nest-dynamic-templates.module.js +40 -96
  66. package/dist/lib/nest-dynamic-templates.module.js.map +1 -1
  67. package/dist/lib/services/template-config.service.d.ts +12 -15
  68. package/dist/lib/services/template-config.service.js +30 -45
  69. package/dist/lib/services/template-config.service.js.map +1 -1
  70. package/dist/lib/services/template-engine.registry.d.ts +6 -10
  71. package/dist/lib/services/template-engine.registry.js +42 -50
  72. package/dist/lib/services/template-engine.registry.js.map +1 -1
  73. package/dist/lib/services/template-layout.service.d.ts +1 -3
  74. package/dist/lib/services/template-layout.service.js +80 -152
  75. package/dist/lib/services/template-layout.service.js.map +1 -1
  76. package/dist/lib/services/template.service.d.ts +1 -3
  77. package/dist/lib/services/template.service.js +107 -209
  78. package/dist/lib/services/template.service.js.map +1 -1
  79. package/dist/tsconfig.build.tsbuildinfo +1 -1
  80. package/package.json +17 -20
package/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## 2.0.0
4
+
5
+ A focused redesign around clearer configuration, diagnostic errors and a lighter install. See [MIGRATION.md](./MIGRATION.md) for upgrade steps.
6
+
7
+ ### Added
8
+ - **Diagnostic render errors.** Failures now throw `TemplateRenderError` carrying a structured `details` payload: the missing variable, the source `location` (line/column), a `snippet` of the offending line, the `contextKeys` you actually passed, and an actionable `hint`. The original error is preserved on `cause`.
9
+ - **Stable error codes** via `TemplateErrorCode` and an `isTemplateError()` guard. Every error extends its matching NestJS HTTP exception (404/422/400/403/409/500).
10
+ - **Flat configuration**: top-level `filters`, `globals` and `engineOptions`.
11
+ - **Custom filters/globals now work across all engines** (Nunjucks, Handlebars, EJS, Pug), not just Nunjucks.
12
+ - **Real Markdown rendering** via the optional `marked` peer.
13
+ - VitePress documentation site (GitHub Pages).
14
+
15
+ ### Changed
16
+ - The `engines` list now **gates which engines are loaded** — only enabled engines are instantiated, making peer dependencies genuinely optional. The list is replaced, not merged with defaults.
17
+ - `TemplateConfigService` is now an injectable instance service (was a global static); this fixes `forRootAsync` timing.
18
+ - Render failures are **HTTP 422** (were 500).
19
+ - `render()` returns `subject: string | null` when there is no subject (was `''`).
20
+ - The HTML processor is a pass-through (the old strict validation that rejected valid output is removed).
21
+ - Engines load lazily; nothing is required until first use.
22
+
23
+ ### Fixed
24
+ - MJML validation errors are now readable (were serialized as `[object Object]`).
25
+ - A missing layout now throws `TemplateNotFoundError` (404) instead of being miscategorized as a generic layout error.
26
+ - Removed double-wrapped error messages.
27
+ - The default HTML render path no longer fails with `"Invalid HTML content"`.
28
+
29
+ ### Removed
30
+ - Required peer dependencies `@faker-js/faker` and `htmlparser2`; internal `yargs` and `deepmerge`.
31
+ - Error classes `TemplateEngineError`, `TemplateLanguageError`, `TemplateLayoutError`, `TemplateContentError`, `TemplateValidationError` (superseded by `TemplateRenderError` + `TemplateInputError`).
32
+ - Inline `content`/`language` fields on `RenderTemplateDto` (use `renderContent()`), and the `renderEngine()`/`renderLanguage()` service helpers.
package/MIGRATION.md ADDED
@@ -0,0 +1,97 @@
1
+ # Migrating to v2
2
+
3
+ v2 is a focused redesign around three goals: **clearer configuration**, **diagnostic errors**, and a **lighter install**. Most apps only need the config rename in step 1.
4
+
5
+ ## 1. Configuration: flatten `enginesOptions`
6
+
7
+ The nested `enginesOptions` block is replaced by top-level fields. The old shape still works (with a one-time deprecation warning), but you should migrate:
8
+
9
+ ```diff
10
+ NestDynamicTemplatesModule.forRoot({
11
+ engines: { template: ['njk'], language: ['html', 'mjml'] },
12
+ - enginesOptions: {
13
+ - filters: { formatDate },
14
+ - globalValues: { brandName: 'Acme' },
15
+ - template: { njk: { autoescape: true } },
16
+ - language: { mjml: { validationLevel: 'soft' } },
17
+ - },
18
+ + filters: { formatDate },
19
+ + globals: { brandName: 'Acme' }, // renamed from globalValues
20
+ + engineOptions: {
21
+ + template: { njk: { autoescape: true } },
22
+ + language: { mjml: { validationLevel: 'soft' } },
23
+ + },
24
+ });
25
+ ```
26
+
27
+ ## 2. The `engines` list now gates loading
28
+
29
+ In v1 the `engines` list was ignored — every engine was loaded regardless. In v2 **only enabled engines are instantiated**, which is what makes the peer dependencies truly optional.
30
+
31
+ - List every engine you use under `engines.template` / `engines.language`.
32
+ - Install only those engines' peer packages.
33
+ - Rendering with a disabled engine throws `TemplateEngineUnavailableError` with a hint.
34
+
35
+ Defaults are unchanged: `{ template: ['njk'], language: ['html', 'mjml', 'txt'] }`.
36
+
37
+ > Note: the engine list is now **replaced**, not merged. In v1 (deepmerge) passing `template: ['hbs']` produced `['njk', 'hbs']`; in v2 you get exactly `['hbs']`.
38
+
39
+ ## 3. Errors are restructured
40
+
41
+ The granular v1 error classes are gone, replaced by a smaller, richer set:
42
+
43
+ | Removed (v1) | Use instead (v2) |
44
+ | --- | --- |
45
+ | `TemplateEngineError`, `TemplateLanguageError`, `TemplateLayoutError`, `TemplateContentError` | `TemplateRenderError` (rich `details`, HTTP **422**) |
46
+ | `TemplateValidationError` | `TemplateInputError` (HTTP 400) |
47
+ | raw `NotFoundException` | `TemplateNotFoundError` (still `instanceof NotFoundException`) |
48
+
49
+ Key changes:
50
+
51
+ - Render failures are now **HTTP 422** (were 500) — they're input/data errors, not server faults.
52
+ - Each error carries `error.code` (a `TemplateErrorCode`) and `error.details` (variable, location, snippet, context keys, hint). The original error is on `error.cause`.
53
+ - Use the `isTemplateError(err)` guard, or switch on `err.code`. Each error still extends its matching NestJS HTTP exception.
54
+
55
+ ```diff
56
+ - } catch (err) {
57
+ - if (err instanceof TemplateEngineError) { /* ... */ }
58
+ + } catch (err) {
59
+ + if (isTemplateError(err)) {
60
+ + console.error(err.code, err.details.missingVariable, err.details.contextKeys);
61
+ + }
62
+ }
63
+ ```
64
+
65
+ ## 4. `TemplateConfigService` is now injectable
66
+
67
+ It was a global static in v1; it's a normal injectable service in v2 (fixes `forRootAsync` timing). The static methods (`setOptions`, `reset`, `hasConfig`, `get`) are removed. Inject it and call instance methods:
68
+
69
+ ```diff
70
+ - TemplateConfigService.getOptions();
71
+ + constructor(private readonly config: TemplateConfigService) {}
72
+ + this.config.getOptions();
73
+ ```
74
+
75
+ ## 5. `render()` no longer accepts inline content
76
+
77
+ `RenderTemplateDto`'s `content` / `language` fields (which never actually worked in `render()`) are removed. To render a raw string, use `renderContent()`:
78
+
79
+ ```diff
80
+ - await templates.render({ content: 'Hi {{ name }}', language: 'html', context });
81
+ + await templates.renderContent({ content: 'Hi {{ name }}', engine: 'njk', language: 'html', context });
82
+ ```
83
+
84
+ The low-level `renderEngine()` / `renderLanguage()` helpers were also removed from the services — use `renderContent()`.
85
+
86
+ ## 6. Output and behavior tweaks
87
+
88
+ - `render()` returns `subject: string | null` (was `''`) when a template has no subject.
89
+ - The **HTML** processor is now a pass-through. v1 ran a strict validation that could reject valid output with `"Invalid HTML content"`; that's gone.
90
+ - **Markdown** now actually renders (via the optional `marked` peer). It was a no-op in v1.
91
+ - Only **active** templates (`isActive: true`) are resolved by `render()`.
92
+
93
+ ## 7. Dependencies
94
+
95
+ - **Removed required peers:** `@faker-js/faker`, `htmlparser2`. Remove them from your install if you only added them for this library.
96
+ - **New optional peer:** `marked` — install it only if you enable the `md` language.
97
+ - No database schema changes — your existing tables are compatible.
package/README.md CHANGED
@@ -1,95 +1,97 @@
1
1
  # @ackplus/nest-dynamic-templates
2
2
 
3
- A powerful and flexible dynamic template rendering library for NestJS applications. Support for multiple template engines (Nunjucks, Handlebars, EJS, Pug) and content languages (HTML, MJML, Markdown, Text), with built-in database storage and layout management.
3
+ > Database-backed, multi-engine template rendering for NestJS with **error messages that actually tell you what went wrong**.
4
4
 
5
- ## Features
5
+ Store templates in your database, render them at runtime with the engine of your choice (Nunjucks, Handlebars, EJS, Pug), and post-process the output as HTML, MJML, Markdown or plain text. Built for transactional email, notifications, PDFs and any per-tenant/per-locale content.
6
6
 
7
- - 🔌 **Multiple Engines** - Support for Nunjucks, Handlebars, EJS, and Pug
8
- - 📝 **Multi-Format** - Render HTML, MJML, Markdown, or Plain Text
9
- - 🗄️ **Database Storage** - Store templates in your database (TypeORM support)
10
- - 🎨 **Layout Support** - Create reusable layouts for your templates
11
- - 🌍 **Scope & Locale** - Manage templates by scope (system/user/tenant) and locale (en/es/etc.)
12
- - 🚀 **Dynamic Rendering** - Render templates with dynamic context at runtime
7
+ 📖 **Full documentation:** https://ack-solutions.github.io/nest-dynamic-templates/
13
8
 
14
- ## 📦 Installation
9
+ ---
15
10
 
16
- ```bash
17
- npm install @ackplus/nest-dynamic-templates
18
- # or
19
- pnpm add @ackplus/nest-dynamic-templates
20
- # or
21
- yarn add @ackplus/nest-dynamic-templates
22
- ```
11
+ ## Why this library
23
12
 
24
- ### Peer Dependencies
13
+ - 🧩 **Two-stage pipeline** — a *template engine* interpolates your variables, then a *language processor* turns the result into final markup. Mix and match (e.g. Nunjucks → MJML).
14
+ - 🗄️ **Database storage** — templates and layouts live in your DB (TypeORM), versioned and editable at runtime.
15
+ - 🌍 **Scope & locale resolution** — ship `system` defaults and let a `user`/`tenant`/`organization` override them, per locale, with automatic fallback.
16
+ - 🪶 **Lightweight & lazy** — only the engines you enable are loaded, so you only install the peers you actually use.
17
+ - 🛑 **Diagnostic errors** — when a render fails you get the **missing variable**, the **source line**, the **context keys you passed**, and an **actionable hint** — not a generic 500.
25
18
 
26
- You must install the necessary peer dependencies depending on which engines and database you use:
19
+ ## Install
27
20
 
28
21
  ```bash
29
- # Core dependencies
30
- npm install @nestjs/common @nestjs/core @nestjs/typeorm typeorm reflect-metadata
31
-
32
- # Template Engines (install at least one)
33
- npm install nunjucks @types/nunjucks
34
- # OR
35
- npm install handlebars
36
- # OR
37
- npm install ejs @types/ejs
38
- # OR
39
- npm install pug @types/pug
40
-
41
- # Language Support (optional)
42
- npm install mjml @types/mjml # For MJML support
43
- npm install htmlparser2 # For HTML processing
22
+ npm install @ackplus/nest-dynamic-templates
23
+ # peer deps you always need:
24
+ npm install @nestjs/typeorm typeorm reflect-metadata
44
25
  ```
45
26
 
46
- ## 🚀 Quick Start
27
+ Then install **only the engines you enable**:
28
+
29
+ | You enable | Install |
30
+ | --- | --- |
31
+ | `njk` (Nunjucks, default) | `npm install nunjucks` |
32
+ | `hbs` (Handlebars) | `npm install handlebars` |
33
+ | `ejs` | `npm install ejs` |
34
+ | `pug` | `npm install pug` |
35
+ | `mjml` (email) | `npm install mjml` |
36
+ | `md` (Markdown) | `npm install marked` |
37
+ | `html`, `txt` | nothing — built in |
47
38
 
48
- ### 1. Import Module
39
+ ## Quick start
49
40
 
50
- Import `NestDynamicTemplatesModule` into your root `AppModule`. You must configure it with `TypeORM`.
41
+ ### 1. Register the module
51
42
 
52
43
  ```typescript
53
44
  import { Module } from '@nestjs/common';
54
45
  import { TypeOrmModule } from '@nestjs/typeorm';
55
- import { NestDynamicTemplatesModule, TemplateEngineEnum, TemplateLanguageEnum } from '@ackplus/nest-dynamic-templates';
46
+ import {
47
+ NestDynamicTemplatesModule,
48
+ TemplateEngineEnum,
49
+ TemplateLanguageEnum,
50
+ } from '@ackplus/nest-dynamic-templates';
56
51
 
57
52
  @Module({
58
53
  imports: [
59
54
  TypeOrmModule.forRoot({
60
- // ... your database config
55
+ type: 'postgres',
56
+ // ...your db config
57
+ autoLoadEntities: true, // picks up the library's entities automatically
61
58
  }),
62
59
  NestDynamicTemplatesModule.forRoot({
60
+ isGlobal: true,
63
61
  engines: {
64
- template: [TemplateEngineEnum.NUNJUCKS], // Enable specific engines
65
- language: [TemplateLanguageEnum.HTML, TemplateLanguageEnum.MJML]
62
+ template: [TemplateEngineEnum.NUNJUCKS],
63
+ language: [TemplateLanguageEnum.HTML, TemplateLanguageEnum.MJML],
66
64
  },
67
- isGlobal: true, // Optional: make module global
68
65
  }),
69
66
  ],
70
67
  })
71
68
  export class AppModule {}
72
69
  ```
73
70
 
74
- ### 2. Create a Template
71
+ > Prefer explicit entities (e.g. for migrations)? Import `NestDynamicTemplatesEntities` and spread it into your TypeORM `entities` array.
75
72
 
76
- You can create templates programmatically using the `TemplateService`.
73
+ ### 2. Create a template
77
74
 
78
75
  ```typescript
79
76
  import { Injectable } from '@nestjs/common';
80
- import { TemplateService, TemplateEngineEnum, TemplateLanguageEnum } from '@ackplus/nest-dynamic-templates';
77
+ import {
78
+ TemplateService,
79
+ TemplateEngineEnum,
80
+ TemplateLanguageEnum,
81
+ } from '@ackplus/nest-dynamic-templates';
81
82
 
82
83
  @Injectable()
83
- export class MyService {
84
- constructor(private readonly templateService: TemplateService) {}
84
+ export class TemplatesSeeder {
85
+ constructor(private readonly templates: TemplateService) {}
85
86
 
86
- async createWelcomeTemplate() {
87
- await this.templateService.createTemplate({
87
+ seed() {
88
+ return this.templates.createTemplate({
88
89
  name: 'welcome-email',
89
- scope: 'system', // 'system' or custom scope
90
+ displayName: 'Welcome email',
91
+ scope: 'system',
90
92
  locale: 'en',
91
- subject: 'Welcome, {{ name }}!',
92
- content: '<h1>Hello {{ name }}</h1><p>Welcome to our platform.</p>',
93
+ subject: 'Welcome, {{ firstName }}!',
94
+ content: '<h1>Hello {{ firstName }}</h1><p>Thanks for joining.</p>',
93
95
  engine: TemplateEngineEnum.NUNJUCKS,
94
96
  language: TemplateLanguageEnum.HTML,
95
97
  type: 'email',
@@ -98,101 +100,184 @@ export class MyService {
98
100
  }
99
101
  ```
100
102
 
101
- ### 3. Render a Template
103
+ ### 3. Render it
104
+
105
+ ```typescript
106
+ const { subject, content } = await this.templates.render({
107
+ name: 'welcome-email',
108
+ scope: 'system',
109
+ locale: 'en',
110
+ context: { firstName: 'Ada' },
111
+ });
112
+ // subject -> "Welcome, Ada!"
113
+ // content -> "<h1>Hello Ada</h1><p>Thanks for joining.</p>"
114
+ ```
115
+
116
+ Need to render a raw string without touching the DB? Use `renderContent()`:
102
117
 
103
- Render a stored template by name.
118
+ ```typescript
119
+ const html = await this.templates.renderContent({
120
+ content: 'Hi {{ name }}',
121
+ engine: TemplateEngineEnum.NUNJUCKS,
122
+ language: TemplateLanguageEnum.HTML,
123
+ context: { name: 'World' },
124
+ });
125
+ ```
126
+
127
+ ## Error handling
128
+
129
+ This is the headline feature. Every render failure throws a typed error with a structured `details` payload and the original error attached as `cause`. Missing-variable failures name the variable and list the context you actually passed:
104
130
 
105
131
  ```typescript
106
- async renderEmail(userName: string) {
107
- const result = await this.templateService.render({
108
- name: 'welcome-email',
109
- scope: 'system',
110
- locale: 'en',
111
- context: {
112
- name: userName,
113
- },
114
- });
115
-
116
- console.log(result.subject); // "Welcome, John!"
117
- console.log(result.content); // "<h1>Hello John</h1><p>Welcome to our platform.</p>"
132
+ import { TemplateRenderError, TemplateErrorCode, isTemplateError } from '@ackplus/nest-dynamic-templates';
133
+
134
+ try {
135
+ await templates.render({ name: 'welcome-email', context: { email: 'a@b.com' } });
136
+ } catch (err) {
137
+ if (isTemplateError(err)) {
138
+ console.error(err.code); // 'TEMPLATE_RENDER_FAILED'
139
+ console.error(err.details.missingVariable); // 'firstName'
140
+ console.error(err.details.contextKeys); // ['email']
141
+ console.error(err.details.location); // { line: 1, column: 16 }
142
+ console.error(err.details.snippet); // '<h1>Hello {{ firstName }}</h1>...'
143
+ console.error(err.details.hint); // 'Pass "firstName" in the render context. ...'
144
+ }
118
145
  }
119
146
  ```
120
147
 
121
- ## 📚 API Reference
148
+ The thrown message reads:
149
+
150
+ ```
151
+ Failed to render template "welcome-email" [njk → html, scope=system, locale=en]: variable "firstName" is undefined.
152
+ ```
122
153
 
123
- ### TemplateService
154
+ Each error also **extends the matching NestJS HTTP exception**, so Nest's exception filter maps the right status automatically and your existing `catch (NotFoundException)` keeps working:
124
155
 
125
- The main service for managing and rendering templates.
156
+ | Error | HTTP | `code` |
157
+ | --- | --- | --- |
158
+ | `TemplateRenderError` | 422 | `TEMPLATE_RENDER_FAILED` |
159
+ | `TemplateNotFoundError` | 404 | `TEMPLATE_NOT_FOUND` |
160
+ | `TemplateInputError` | 400 | `TEMPLATE_INVALID_INPUT` |
161
+ | `TemplateForbiddenError` | 403 | `TEMPLATE_FORBIDDEN` |
162
+ | `TemplateConflictError` | 409 | `TEMPLATE_CONFLICT` |
163
+ | `TemplateEngineUnavailableError` | 500 | `TEMPLATE_ENGINE_UNAVAILABLE` |
126
164
 
127
- #### `render(options: RenderTemplateDto)`
128
- Renders a template stored in the database.
165
+ ## Configuration
129
166
 
130
167
  ```typescript
131
- const output = await templateService.render({
132
- name: 'my-template',
133
- scope: 'system',
134
- locale: 'en',
135
- context: { foo: 'bar' },
168
+ NestDynamicTemplatesModule.forRoot({
169
+ isGlobal: true,
170
+
171
+ // Only these engines are loaded. Default: { template: ['njk'], language: ['html','mjml','txt'] }
172
+ engines: {
173
+ template: [TemplateEngineEnum.NUNJUCKS, TemplateEngineEnum.HANDLEBARS],
174
+ language: [TemplateLanguageEnum.HTML, TemplateLanguageEnum.MJML, TemplateLanguageEnum.MARKDOWN],
175
+ },
176
+
177
+ // Custom filters/helpers — available in EVERY template engine.
178
+ filters: {
179
+ formatDate: (d: Date, fmt: string) => /* ... */ '',
180
+ formatCurrency: (n: number, ccy: string) =>
181
+ new Intl.NumberFormat('en-US', { style: 'currency', currency: ccy }).format(n),
182
+ },
183
+
184
+ // Global values injected into every render (strings, numbers, objects or functions).
185
+ globals: {
186
+ brandName: 'Acme',
187
+ year: () => new Date().getFullYear(),
188
+ },
189
+
190
+ // Raw options forwarded to the underlying engine libraries.
191
+ engineOptions: {
192
+ template: { njk: { autoescape: true, trimBlocks: true } },
193
+ language: { mjml: { validationLevel: 'soft' } },
194
+ },
136
195
  });
137
196
  ```
138
197
 
139
- #### `renderContent(options: RenderContentTemplateDto)`
140
- Renders raw content string directly without fetching from the database.
198
+ ### Async configuration
141
199
 
142
200
  ```typescript
143
- const html = await templateService.renderContent({
144
- content: 'Hello {{ name }}',
145
- engine: TemplateEngineEnum.NUNJUCKS,
146
- context: { name: 'World' },
201
+ NestDynamicTemplatesModule.forRootAsync({
202
+ isGlobal: true,
203
+ inject: [ConfigService],
204
+ useFactory: (config: ConfigService) => ({
205
+ engines: { template: ['njk'], language: ['html', 'mjml'] },
206
+ globals: { appUrl: config.get('APP_URL') },
207
+ }),
147
208
  });
148
209
  ```
149
210
 
150
- #### `createTemplate(data: CreateTemplateDto)`
151
- Creates a new system template.
211
+ ## Scope & locale resolution
212
+
213
+ `render()` resolves a template by trying, in order:
214
+
215
+ 1. requested **scope** + requested **locale**
216
+ 2. requested **scope** + `en`
217
+ 3. **system** scope + requested locale
218
+ 4. **system** scope + `en`
219
+
220
+ This lets you ship `system` defaults and override them per tenant/user and per language. Only **active** templates (`isActive: true`) are resolved. Example:
221
+
222
+ ```typescript
223
+ // Falls back to the system template if this user has no override.
224
+ await templates.render({ name: 'welcome-email', scope: 'user', scopeId: userId, locale: 'fr' });
225
+ ```
152
226
 
153
- #### `updateTemplate(id: string, updates: Partial<CreateTemplateDto>)`
154
- Updates an existing template. If you try to update a `system` template without permission, it may create a scoped override instead.
227
+ ## Template fields
155
228
 
156
- ### TemplateLayoutService
229
+ A template is a database row. The key fields (full reference with use cases in the [docs](https://ack-solutions.github.io/nest-dynamic-templates/reference/template-fields)):
157
230
 
158
- Manage reusable layouts (e.g., email wrappers with header/footer).
231
+ | Field | Required | Default | Purpose |
232
+ | --- | --- | --- | --- |
233
+ | `name` | ✅ | — | Stable id you render by, e.g. `welcome-email`. |
234
+ | `scope` | — | `system` | **Who owns this version** — `system` (shared default) or a custom owner kind like `tenant`/`user`. |
235
+ | `scopeId` | — | `null` | **Which owner** inside the scope (e.g. the tenant id). Empty for `system`. |
236
+ | `locale` | — | `en` | Language variant; missing locales fall back to `en`. |
237
+ | `engine` | — | `njk` | Template engine: `njk`, `hbs`, `ejs`, `pug`. |
238
+ | `language` | — | `null` | Output processor: `html`, `mjml`, `md`, `txt`. |
239
+ | `subject` | — | `null` | Subject line (emails); rendered with the engine. |
240
+ | `content` | ✅ | — | The template body. |
241
+ | `templateLayoutName` | — | `null` | Layout to wrap this content in. |
242
+ | `displayName` | — | — | Human-friendly label for admin UIs. |
243
+ | `type` | — | `null` | Free category for grouping, e.g. `email`/`sms`. |
244
+ | `previewContext` | — | `null` | Sample data for previews (not used at render time). |
245
+ | `isActive` | — | `true` | Set `false` to disable without deleting. |
159
246
 
160
- #### `createLayout(data: CreateTemplateLayoutDto)`
161
- Create a new layout.
247
+ **`scope` vs `scopeId`** is the part to understand: `scope` is the *kind* of owner (`system`, or your own `tenant`/`user`/…), and `scopeId` is the *exact* owner (the tenant id). Together with `name` + `locale` they uniquely identify a template, and a render falls back from a specific override to the `system` default — so you only store overrides where they differ.
248
+
249
+ ## Layouts
250
+
251
+ Layouts are reusable wrappers (e.g. an email shell). The child content is injected where the layout references `{{ content }}`:
162
252
 
163
253
  ```typescript
164
- await layoutService.createLayout({
165
- name: 'main-layout',
166
- content: '<html><body>{{ content }}</body></html>', // {{ content }} is the placeholder
254
+ await layouts.createTemplateLayout({
255
+ name: 'email-shell',
256
+ displayName: 'Email shell',
167
257
  engine: TemplateEngineEnum.NUNJUCKS,
258
+ language: TemplateLanguageEnum.HTML,
259
+ content: '<html><body><header>{{ brandName }}</header>{{ content }}</body></html>',
260
+ });
261
+
262
+ // Attach it to a template:
263
+ await templates.createTemplate({
264
+ name: 'welcome-email',
265
+ templateLayoutName: 'email-shell',
266
+ /* ...rest... */
168
267
  });
169
268
  ```
170
269
 
171
- ## ⚙️ Configuration Options
270
+ ## Services
172
271
 
173
- When importing the module, you can configure the enabled engines:
272
+ - **`TemplateService`** `render`, `renderContent`, `createTemplate`, `updateTemplate`, `overwriteSystemTemplate`, `deleteTemplate`, `getTemplates`, `findTemplate`, `getTemplateById`.
273
+ - **`TemplateLayoutService`** — the same surface for layouts.
274
+ - **`TemplateConfigService`** — read-only accessor over the resolved config.
275
+ - **`TemplateEngineRegistryService`** — access the engine instances directly.
174
276
 
175
- ```typescript
176
- NestDynamicTemplatesModule.forRoot({
177
- engines: {
178
- // Template Logic Engines
179
- template: [
180
- TemplateEngineEnum.NUNJUCKS,
181
- TemplateEngineEnum.HANDLEBARS,
182
- TemplateEngineEnum.EJS,
183
- TemplateEngineEnum.PUG
184
- ],
185
- // Output Language Processors
186
- language: [
187
- TemplateLanguageEnum.HTML,
188
- TemplateLanguageEnum.MJML,
189
- TemplateLanguageEnum.TEXT,
190
- TemplateLanguageEnum.MARKDOWN
191
- ]
192
- }
193
- })
194
- ```
277
+ ## Migrating from v1
278
+
279
+ v2 is a focused redesign. See **[MIGRATION.md](./MIGRATION.md)** for the (short) list of breaking changes and how to update — most apps only need to rename `enginesOptions` to the new flat `filters` / `globals` / `engineOptions`.
195
280
 
196
- ## 📄 License
281
+ ## License
197
282
 
198
- This project is licensed under the MIT License.
283
+ MIT © AckPlus
package/dist/index.d.ts CHANGED
@@ -6,9 +6,11 @@ export * from './lib/services/template-config.service';
6
6
  export * from './lib/services/template-engine.registry';
7
7
  export * from './lib/interfaces/module-config.interface';
8
8
  export * from './lib/interfaces/template.types';
9
+ export * from './lib/config/resolve-config';
9
10
  export * from './lib/entities/template.entity';
10
11
  export * from './lib/entities/template-layout.entity';
11
12
  export * from './lib/errors/template.errors';
13
+ export * from './lib/errors/diagnose';
12
14
  export * from './lib/constant';
13
15
  export * from './lib/engines/template-engine';
14
16
  export * from './lib/engines/language-engine';
package/dist/index.js CHANGED
@@ -24,9 +24,11 @@ __exportStar(require("./lib/services/template-config.service"), exports);
24
24
  __exportStar(require("./lib/services/template-engine.registry"), exports);
25
25
  __exportStar(require("./lib/interfaces/module-config.interface"), exports);
26
26
  __exportStar(require("./lib/interfaces/template.types"), exports);
27
+ __exportStar(require("./lib/config/resolve-config"), exports);
27
28
  __exportStar(require("./lib/entities/template.entity"), exports);
28
29
  __exportStar(require("./lib/entities/template-layout.entity"), exports);
29
30
  __exportStar(require("./lib/errors/template.errors"), exports);
31
+ __exportStar(require("./lib/errors/diagnose"), exports);
30
32
  __exportStar(require("./lib/constant"), exports);
31
33
  __exportStar(require("./lib/engines/template-engine"), exports);
32
34
  __exportStar(require("./lib/engines/language-engine"), exports);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,kFAAkF;AAClF,oEAAqE;AAErE,sEAAoD;AAGpD,kEAAgD;AAChD,yEAAuD;AACvD,yEAAuD;AACvD,0EAAwD;AAGxD,2EAAyD;AACzD,kEAAgD;AAGhD,iEAA+C;AAC/C,wEAAsD;AAGtD,+DAA6C;AAG7C,iDAA+B;AAG/B,gEAA8C;AAC9C,gEAA8C;AAG9C,gEAA8C;AAC9C,gEAA8C;AAC9C,wEAAsD;AACtD,gEAA8C;AAC9C,uEAAqD;AACrD,uEAAqD;AACrD,+EAA6D;AAC7D,uEAAqD;AAGxC,QAAA,4BAA4B,GAAG;IACxC,qCAAmB;IACnB,kDAAyB;CAC5B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,kFAAkF;AAClF,oEAAqE;AAErE,sEAAoD;AAGpD,kEAAgD;AAChD,yEAAuD;AACvD,yEAAuD;AACvD,0EAAwD;AAGxD,2EAAyD;AACzD,kEAAgD;AAChD,8DAA4C;AAG5C,iEAA+C;AAC/C,wEAAsD;AAGtD,+DAA6C;AAC7C,wDAAsC;AAGtC,iDAA+B;AAG/B,gEAA8C;AAC9C,gEAA8C;AAG9C,gEAA8C;AAC9C,gEAA8C;AAC9C,wEAAsD;AACtD,gEAA8C;AAC9C,uEAAqD;AACrD,uEAAqD;AACrD,+EAA6D;AAC7D,uEAAqD;AAExC,QAAA,4BAA4B,GAAG;IACxC,qCAAmB;IACnB,kDAAyB;CAC5B,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { TemplateEngineEnum, TemplateLanguageEnum } from '../interfaces/template.types';
2
+ import { NestDynamicTemplatesModuleConfig, ResolvedTemplatesConfig } from '../interfaces/module-config.interface';
3
+ export declare const DEFAULT_ENGINES: {
4
+ template: TemplateEngineEnum[];
5
+ language: TemplateLanguageEnum[];
6
+ };
7
+ export declare function resolveConfig(config?: NestDynamicTemplatesModuleConfig): ResolvedTemplatesConfig;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_ENGINES = void 0;
4
+ exports.resolveConfig = resolveConfig;
5
+ const template_types_1 = require("../interfaces/template.types");
6
+ exports.DEFAULT_ENGINES = {
7
+ template: [template_types_1.TemplateEngineEnum.NUNJUCKS],
8
+ language: [
9
+ template_types_1.TemplateLanguageEnum.HTML,
10
+ template_types_1.TemplateLanguageEnum.MJML,
11
+ template_types_1.TemplateLanguageEnum.TEXT,
12
+ ],
13
+ };
14
+ let warnedAboutLegacy = false;
15
+ function resolveConfig(config = {}) {
16
+ const legacy = mapLegacy(config);
17
+ return {
18
+ isGlobal: config.isGlobal ?? false,
19
+ engines: {
20
+ template: dedupe(config.engines?.template ?? exports.DEFAULT_ENGINES.template),
21
+ language: dedupe(config.engines?.language ?? exports.DEFAULT_ENGINES.language),
22
+ },
23
+ filters: { ...legacy.filters, ...config.filters },
24
+ globals: { ...legacy.globals, ...config.globals },
25
+ engineOptions: {
26
+ template: { ...legacy.engineOptions.template, ...config.engineOptions?.template },
27
+ language: { ...legacy.engineOptions.language, ...config.engineOptions?.language },
28
+ },
29
+ };
30
+ }
31
+ function mapLegacy(config) {
32
+ const legacy = config.enginesOptions;
33
+ if (legacy && !warnedAboutLegacy) {
34
+ warnedAboutLegacy = true;
35
+ console.warn('[nest-dynamic-templates] `enginesOptions` is deprecated. Use top-level ' +
36
+ '`filters`, `globals` and `engineOptions` instead. See the v2 migration guide.');
37
+ }
38
+ return {
39
+ filters: legacy?.filters ?? {},
40
+ globals: legacy?.globalValues ?? {},
41
+ engineOptions: {
42
+ template: legacy?.template ?? {},
43
+ language: legacy?.language ?? {},
44
+ },
45
+ };
46
+ }
47
+ function dedupe(items) {
48
+ return Array.from(new Set(items));
49
+ }
50
+ //# sourceMappingURL=resolve-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-config.js","sourceRoot":"","sources":["../../../src/lib/config/resolve-config.ts"],"names":[],"mappings":";;;AAyBA,sCAkBC;AA3CD,iEAAwF;AAO3E,QAAA,eAAe,GAAG;IAC3B,QAAQ,EAAE,CAAC,mCAAkB,CAAC,QAAQ,CAAyB;IAC/D,QAAQ,EAAE;QACN,qCAAoB,CAAC,IAAI;QACzB,qCAAoB,CAAC,IAAI;QACzB,qCAAoB,CAAC,IAAI;KACF;CAC9B,CAAC;AAEF,IAAI,iBAAiB,GAAG,KAAK,CAAC;AAS9B,SAAgB,aAAa,CACzB,SAA2C,EAAE;IAE7C,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAEjC,OAAO;QACH,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,KAAK;QAClC,OAAO,EAAE;YACL,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,IAAI,uBAAe,CAAC,QAAQ,CAAC;YACtE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,IAAI,uBAAe,CAAC,QAAQ,CAAC;SACzE;QACD,OAAO,EAAE,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE;QACjD,OAAO,EAAE,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE;QACjD,aAAa,EAAE;YACX,QAAQ,EAAE,EAAE,GAAG,MAAM,CAAC,aAAa,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,aAAa,EAAE,QAAQ,EAAE;YACjF,QAAQ,EAAE,EAAE,GAAG,MAAM,CAAC,aAAa,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,aAAa,EAAE,QAAQ,EAAE;SACpF;KACJ,CAAC;AACN,CAAC;AAGD,SAAS,SAAS,CAAC,MAAwC;IACvD,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC;IACrC,IAAI,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC/B,iBAAiB,GAAG,IAAI,CAAC;QAEzB,OAAO,CAAC,IAAI,CACR,yEAAyE;YACrE,+EAA+E,CACtF,CAAC;IACN,CAAC;IACD,OAAO;QACH,OAAO,EAAE,MAAM,EAAE,OAAO,IAAI,EAAE;QAC9B,OAAO,EAAE,MAAM,EAAE,YAAY,IAAI,EAAE;QACnC,aAAa,EAAE;YACX,QAAQ,EAAE,MAAM,EAAE,QAAQ,IAAI,EAAE;YAChC,QAAQ,EAAE,MAAM,EAAE,QAAQ,IAAI,EAAE;SACnC;KACJ,CAAC;AACN,CAAC;AAED,SAAS,MAAM,CAAI,KAAU;IACzB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AACtC,CAAC"}
@@ -1,2 +1,3 @@
1
+ export declare const NEST_DYNAMIC_TEMPLATES_OPTIONS: unique symbol;
1
2
  export declare const NEST_DYNAMIC_TEMPLATES_MODULE_CONFIG: unique symbol;
2
3
  export declare const NEST_DYNAMIC_TEMPLATES_ASYNC_OPTIONS_PROVIDER = "NEST_DYNAMIC_TEMPLATES_ASYNC_OPTIONS_PROVIDER";
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NEST_DYNAMIC_TEMPLATES_ASYNC_OPTIONS_PROVIDER = exports.NEST_DYNAMIC_TEMPLATES_MODULE_CONFIG = void 0;
3
+ exports.NEST_DYNAMIC_TEMPLATES_ASYNC_OPTIONS_PROVIDER = exports.NEST_DYNAMIC_TEMPLATES_MODULE_CONFIG = exports.NEST_DYNAMIC_TEMPLATES_OPTIONS = void 0;
4
+ exports.NEST_DYNAMIC_TEMPLATES_OPTIONS = Symbol('NEST_DYNAMIC_TEMPLATES_OPTIONS');
4
5
  exports.NEST_DYNAMIC_TEMPLATES_MODULE_CONFIG = Symbol('NEST_DYNAMIC_TEMPLATES_MODULE_CONFIG');
5
6
  exports.NEST_DYNAMIC_TEMPLATES_ASYNC_OPTIONS_PROVIDER = 'NEST_DYNAMIC_TEMPLATES_ASYNC_OPTIONS_PROVIDER';
6
7
  //# sourceMappingURL=constant.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"constant.js","sourceRoot":"","sources":["../../src/lib/constant.ts"],"names":[],"mappings":";;;AAAa,QAAA,oCAAoC,GAAG,MAAM,CAAC,sCAAsC,CAAC,CAAC;AACtF,QAAA,6CAA6C,GAAG,+CAA+C,CAAC"}
1
+ {"version":3,"file":"constant.js","sourceRoot":"","sources":["../../src/lib/constant.ts"],"names":[],"mappings":";;;AACa,QAAA,8BAA8B,GAAG,MAAM,CAAC,gCAAgC,CAAC,CAAC;AAG1E,QAAA,oCAAoC,GAAG,MAAM,CAAC,sCAAsC,CAAC,CAAC;AAGtF,QAAA,6CAA6C,GAAG,+CAA+C,CAAC"}