@zyther/tzmail 0.1.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +412 -79
- package/dist/core/enums/theme.enum.d.ts +11 -1
- package/dist/core/enums/theme.enum.d.ts.map +1 -1
- package/dist/core/enums/theme.enum.js +10 -0
- package/dist/core/interfaces/email.interface.d.ts +95 -5
- package/dist/core/interfaces/email.interface.d.ts.map +1 -1
- package/dist/core/interfaces/template.interface.d.ts +12 -1
- package/dist/core/interfaces/template.interface.d.ts.map +1 -1
- package/dist/factories/email-factory.d.ts +32 -6
- package/dist/factories/email-factory.d.ts.map +1 -1
- package/dist/factories/email-factory.js +110 -6
- package/dist/factories/template-factory.d.ts.map +1 -1
- package/dist/factories/template-factory.js +170 -60
- package/dist/index.d.ts +16 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -1
- package/dist/providers/mailgun.transport.d.ts +10 -0
- package/dist/providers/mailgun.transport.d.ts.map +1 -0
- package/dist/providers/mailgun.transport.js +85 -0
- package/dist/providers/nodemailer.transport.d.ts +11 -0
- package/dist/providers/nodemailer.transport.d.ts.map +1 -0
- package/dist/providers/nodemailer.transport.js +23 -0
- package/dist/providers/provider-utils.d.ts +17 -0
- package/dist/providers/provider-utils.d.ts.map +1 -0
- package/dist/providers/provider-utils.js +89 -0
- package/dist/providers/provider.interface.d.ts +30 -0
- package/dist/providers/provider.interface.d.ts.map +1 -0
- package/dist/providers/provider.interface.js +2 -0
- package/dist/providers/sendgrid.transport.d.ts +10 -0
- package/dist/providers/sendgrid.transport.d.ts.map +1 -0
- package/dist/providers/sendgrid.transport.js +76 -0
- package/dist/queues/bullmq-email-queue.d.ts +153 -0
- package/dist/queues/bullmq-email-queue.d.ts.map +1 -0
- package/dist/queues/bullmq-email-queue.js +292 -0
- package/dist/services/attachment.service.d.ts +15 -37
- package/dist/services/attachment.service.d.ts.map +1 -1
- package/dist/services/attachment.service.js +58 -46
- package/dist/services/email.service.d.ts +22 -22
- package/dist/services/email.service.d.ts.map +1 -1
- package/dist/services/email.service.js +117 -33
- package/dist/services/template.service.d.ts +24 -111
- package/dist/services/template.service.d.ts.map +1 -1
- package/dist/services/template.service.js +273 -362
- package/dist/templates/base/color-accessibility.d.ts +4 -0
- package/dist/templates/base/color-accessibility.d.ts.map +1 -0
- package/dist/templates/base/color-accessibility.js +38 -0
- package/dist/templates/base/template-builder.d.ts +4 -1
- package/dist/templates/base/template-builder.d.ts.map +1 -1
- package/dist/templates/base/template-builder.js +276 -527
- package/dist/templates/themes/aurora.theme.d.ts +12 -0
- package/dist/templates/themes/aurora.theme.d.ts.map +1 -0
- package/dist/templates/themes/aurora.theme.js +88 -0
- package/dist/templates/themes/corporate.theme.d.ts +2 -1
- package/dist/templates/themes/corporate.theme.d.ts.map +1 -1
- package/dist/templates/themes/corporate.theme.js +139 -102
- package/dist/templates/themes/editorial.theme.d.ts +12 -0
- package/dist/templates/themes/editorial.theme.d.ts.map +1 -0
- package/dist/templates/themes/editorial.theme.js +90 -0
- package/dist/templates/themes/forest.theme.d.ts +12 -0
- package/dist/templates/themes/forest.theme.d.ts.map +1 -0
- package/dist/templates/themes/forest.theme.js +89 -0
- package/dist/templates/themes/glass.theme.d.ts +12 -0
- package/dist/templates/themes/glass.theme.d.ts.map +1 -0
- package/dist/templates/themes/glass.theme.js +88 -0
- package/dist/templates/themes/lavender.theme.d.ts +12 -0
- package/dist/templates/themes/lavender.theme.d.ts.map +1 -0
- package/dist/templates/themes/lavender.theme.js +91 -0
- package/dist/templates/themes/midnight.theme.d.ts +12 -0
- package/dist/templates/themes/midnight.theme.d.ts.map +1 -0
- package/dist/templates/themes/midnight.theme.js +91 -0
- package/dist/templates/themes/minimal.theme.d.ts +2 -1
- package/dist/templates/themes/minimal.theme.d.ts.map +1 -1
- package/dist/templates/themes/minimal.theme.js +142 -107
- package/dist/templates/themes/modern.theme.d.ts +2 -1
- package/dist/templates/themes/modern.theme.d.ts.map +1 -1
- package/dist/templates/themes/modern.theme.js +109 -74
- package/dist/templates/themes/monokai.theme.d.ts +2 -1
- package/dist/templates/themes/monokai.theme.d.ts.map +1 -1
- package/dist/templates/themes/monokai.theme.js +102 -66
- package/dist/templates/themes/neobrutal.theme.d.ts +12 -0
- package/dist/templates/themes/neobrutal.theme.d.ts.map +1 -0
- package/dist/templates/themes/neobrutal.theme.js +91 -0
- package/dist/templates/themes/ocean.theme.d.ts +12 -0
- package/dist/templates/themes/ocean.theme.d.ts.map +1 -0
- package/dist/templates/themes/ocean.theme.js +90 -0
- package/dist/templates/themes/pastel.theme.d.ts +12 -0
- package/dist/templates/themes/pastel.theme.d.ts.map +1 -0
- package/dist/templates/themes/pastel.theme.js +89 -0
- package/dist/templates/themes/sunset.theme.d.ts +12 -0
- package/dist/templates/themes/sunset.theme.d.ts.map +1 -0
- package/dist/templates/themes/sunset.theme.js +90 -0
- package/dist/templates/themes/system.theme.d.ts +2 -1
- package/dist/templates/themes/system.theme.d.ts.map +1 -1
- package/dist/templates/themes/system.theme.js +81 -47
- package/dist/templates/themes/theme.interface.d.ts +48 -0
- package/dist/templates/themes/theme.interface.d.ts.map +1 -1
- package/package.json +36 -14
- package/src/core/enums/template-part.enum.ts +7 -0
- package/src/core/enums/theme.enum.ts +18 -0
- package/src/core/interfaces/email.interface.ts +116 -0
- package/src/core/interfaces/template.interface.ts +71 -0
- package/src/factories/email-factory.ts +208 -0
- package/src/factories/template-factory.ts +201 -0
- package/src/index.ts +53 -0
- package/src/providers/mailgun.transport.ts +80 -0
- package/src/providers/nodemailer.transport.ts +20 -0
- package/src/providers/provider-utils.ts +91 -0
- package/src/providers/provider.interface.ts +26 -0
- package/src/providers/sendgrid.transport.ts +80 -0
- package/src/queues/bullmq-email-queue.ts +418 -0
- package/src/services/attachment.service.ts +98 -0
- package/src/services/email.service.ts +151 -0
- package/src/services/template.service.ts +431 -0
- package/src/templates/base/color-accessibility.ts +31 -0
- package/src/templates/base/template-builder.ts +438 -0
- package/src/templates/themes/aurora.theme.ts +91 -0
- package/src/templates/themes/corporate.theme.ts +156 -0
- package/src/templates/themes/editorial.theme.ts +93 -0
- package/src/templates/themes/forest.theme.ts +92 -0
- package/src/templates/themes/glass.theme.ts +91 -0
- package/src/templates/themes/lavender.theme.ts +94 -0
- package/src/templates/themes/midnight.theme.ts +94 -0
- package/src/templates/themes/minimal.theme.ts +158 -0
- package/src/templates/themes/modern.theme.ts +124 -0
- package/src/templates/themes/monokai.theme.ts +117 -0
- package/src/templates/themes/neobrutal.theme.ts +94 -0
- package/src/templates/themes/ocean.theme.ts +93 -0
- package/src/templates/themes/pastel.theme.ts +92 -0
- package/src/templates/themes/sunset.theme.ts +93 -0
- package/src/templates/themes/system.theme.ts +92 -0
- package/src/templates/themes/theme.interface.ts +95 -0
- package/src/tests/template.service.test.ts +1 -0
|
@@ -1,506 +1,417 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TemplateService = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
4
5
|
const template_factory_1 = require("../factories/template-factory");
|
|
5
6
|
const theme_enum_1 = require("../core/enums/theme.enum");
|
|
7
|
+
const VALID_THEMES = Object.values(theme_enum_1.ThemeType);
|
|
8
|
+
const SAFE_LANGUAGE_TAG = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/;
|
|
9
|
+
const SAFE_COLOR = /^(#[\da-f]{3,8}|[a-z]{1,24}|(?:rgb|rgba|hsl|hsla)\([\d.%\s,+/-]+\))$/i;
|
|
10
|
+
const SAFE_COLOR_NAMES = new Set(('transparent inherit currentcolor aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgrey darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray grey green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgrey lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen').split(' '));
|
|
11
|
+
const SOCIAL_PLATFORMS = new Set([
|
|
12
|
+
'facebook', 'twitter', 'linkedin', 'github', 'instagram', 'youtube', 'tiktok', 'pinterest', 'reddit',
|
|
13
|
+
'gitlab', 'stackoverflow', 'medium', 'dribbble', 'behance', 'telegram'
|
|
14
|
+
]);
|
|
15
|
+
function cloneConfig(value) {
|
|
16
|
+
if (value === undefined || value === null)
|
|
17
|
+
return value;
|
|
18
|
+
return JSON.parse(JSON.stringify(value));
|
|
19
|
+
}
|
|
20
|
+
function canonicalize(value) {
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return value.map(canonicalize);
|
|
23
|
+
if (value && typeof value === 'object') {
|
|
24
|
+
return Object.keys(value).sort().reduce((result, key) => {
|
|
25
|
+
result[key] = canonicalize(value[key]);
|
|
26
|
+
return result;
|
|
27
|
+
}, {});
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function isSafeColor(value) {
|
|
32
|
+
if (typeof value !== 'string' || !SAFE_COLOR.test(value.trim()))
|
|
33
|
+
return false;
|
|
34
|
+
const normalized = value.trim().toLowerCase();
|
|
35
|
+
return normalized.startsWith('#') || normalized.startsWith('rgb') || normalized.startsWith('hsl') || SAFE_COLOR_NAMES.has(normalized);
|
|
36
|
+
}
|
|
37
|
+
function hasTemplatePlaceholder(value) {
|
|
38
|
+
return typeof value === 'string' && /\{\{\s*[A-Za-z0-9_.-]+\s*\}\}/.test(value);
|
|
39
|
+
}
|
|
40
|
+
function assertSafeUrl(value, allowMailto, field) {
|
|
41
|
+
if (typeof value !== 'string' || !value.trim() || /[\u0000-\u0020"'<>\\]/.test(value)) {
|
|
42
|
+
throw new Error(`${field} must be a valid URL`);
|
|
43
|
+
}
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = new URL(value);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error(`${field} must be an absolute URL`);
|
|
50
|
+
}
|
|
51
|
+
const allowed = allowMailto ? ['http:', 'https:', 'mailto:'] : ['http:', 'https:'];
|
|
52
|
+
if (!allowed.includes(parsed.protocol.toLowerCase()))
|
|
53
|
+
throw new Error(`${field} uses an unsafe URL scheme`);
|
|
54
|
+
}
|
|
6
55
|
/**
|
|
7
|
-
* TemplateService -
|
|
8
|
-
*
|
|
9
|
-
* Métodos:
|
|
10
|
-
* - createTemplate(themeType, variant, config, options): ITemplate
|
|
11
|
-
* Cria um novo template com as configurações especificadas.
|
|
12
|
-
*
|
|
13
|
-
* - cloneTemplate(template, modifications): ITemplate
|
|
14
|
-
* Cria um template a partir de um template existente (clone).
|
|
15
|
-
*
|
|
16
|
-
* - renderTemplate(template, data, options): Promise<string>
|
|
17
|
-
* Renderiza um template com os dados fornecidos.
|
|
18
|
-
*
|
|
19
|
-
* - previewTemplate(themeType, variant, config, data): Promise<string>
|
|
20
|
-
* Pré-visualiza um template sem enviar email.
|
|
21
|
-
*
|
|
22
|
-
* - getThemeInfo(themeType): object
|
|
23
|
-
* Obtém informações sobre um tema específico.
|
|
24
|
-
*
|
|
25
|
-
* - listThemes(): ThemeType[]
|
|
26
|
-
* Lista todos os temas disponíveis.
|
|
27
|
-
*
|
|
28
|
-
* - getTemplateStats(): object
|
|
29
|
-
* Obtém estatísticas de uso dos templates.
|
|
30
|
-
*
|
|
31
|
-
* - clearCache(themeType?): number
|
|
32
|
-
* Limpa o cache de templates, opcionalmente por tema.
|
|
33
|
-
*
|
|
34
|
-
* - getTemplateHistory(templateId): ITemplate[]
|
|
35
|
-
* Obtém histórico de versões de um template.
|
|
36
|
-
*
|
|
37
|
-
* - restoreTemplateVersion(templateId, versionIndex): ITemplate | null
|
|
38
|
-
* Restaura uma versão anterior do template.
|
|
39
|
-
*
|
|
40
|
-
* Exemplo de uso:
|
|
41
|
-
*
|
|
42
|
-
* const templateService = new TemplateService();
|
|
43
|
-
* const myTemplate = templateService.createTemplate(ThemeType.MODERN, 'light', { header: { show: true } });
|
|
44
|
-
* const html = await templateService.renderTemplate(myTemplate, { username: 'John' });
|
|
45
|
-
* console.log(html);
|
|
56
|
+
* TemplateService - criação, renderização e gestão de templates de email.
|
|
57
|
+
* Cache e histórico mantêm limites finitos e retornam snapshots defensivos.
|
|
46
58
|
*/
|
|
47
59
|
class TemplateService {
|
|
60
|
+
templateCache = new Map();
|
|
61
|
+
templateHistory = new Map();
|
|
62
|
+
cacheRequests = 0;
|
|
63
|
+
cacheHits = 0;
|
|
64
|
+
options;
|
|
65
|
+
cleanupTimer;
|
|
48
66
|
constructor(options = {}) {
|
|
49
|
-
this.options = options;
|
|
50
|
-
this.templateCache = new Map();
|
|
51
|
-
this.defaultTTL = 3600;
|
|
52
|
-
this.templateHistory = new Map();
|
|
53
67
|
this.options = {
|
|
54
68
|
cache: true,
|
|
55
69
|
cacheTTL: 3600,
|
|
56
70
|
validateConfig: true,
|
|
57
|
-
minify:
|
|
71
|
+
minify: false,
|
|
58
72
|
preview: false,
|
|
73
|
+
maxCacheEntries: 100,
|
|
74
|
+
maxHistoryEntries: 100,
|
|
75
|
+
maxVersionsPerTemplate: 10,
|
|
59
76
|
...options
|
|
60
77
|
};
|
|
61
|
-
|
|
78
|
+
this.validateLimit(this.options.maxCacheEntries, 'maxCacheEntries');
|
|
79
|
+
this.validateLimit(this.options.maxHistoryEntries, 'maxHistoryEntries');
|
|
80
|
+
this.validateLimit(this.options.maxVersionsPerTemplate, 'maxVersionsPerTemplate');
|
|
81
|
+
this.cleanupTimer = setInterval(() => this.cleanExpiredCache(), 3600000);
|
|
82
|
+
this.cleanupTimer.unref?.();
|
|
62
83
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Cria um novo template com as configurações especificadas
|
|
65
|
-
*/
|
|
66
84
|
createTemplate(themeType, variant, config, options) {
|
|
67
|
-
|
|
68
|
-
if (
|
|
85
|
+
this.assertThemeAndVariant(themeType, variant);
|
|
86
|
+
if (!config || typeof config !== 'object' || Array.isArray(config))
|
|
87
|
+
throw new Error('Template config must be an object');
|
|
88
|
+
if (options?.validateConfig ?? this.options.validateConfig)
|
|
69
89
|
this.validateTemplateConfig(config);
|
|
70
|
-
}
|
|
71
|
-
// Gerar ID único para o template
|
|
72
90
|
const templateId = this.generateTemplateId(themeType, variant, config);
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
const cacheEnabled = options?.cache ?? this.options.cache;
|
|
92
|
+
const ttlSeconds = options?.cacheTTL ?? this.options.cacheTTL;
|
|
93
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds < 0)
|
|
94
|
+
throw new Error('cacheTTL must be a finite non-negative number');
|
|
95
|
+
if (options?.maxCacheEntries !== undefined)
|
|
96
|
+
this.validateLimit(options.maxCacheEntries, 'maxCacheEntries');
|
|
97
|
+
if (options?.maxHistoryEntries !== undefined)
|
|
98
|
+
this.validateLimit(options.maxHistoryEntries, 'maxHistoryEntries');
|
|
99
|
+
if (options?.maxVersionsPerTemplate !== undefined)
|
|
100
|
+
this.validateLimit(options.maxVersionsPerTemplate, 'maxVersionsPerTemplate');
|
|
101
|
+
const useCache = cacheEnabled && ttlSeconds > 0;
|
|
102
|
+
if (useCache) {
|
|
103
|
+
this.cacheRequests++;
|
|
75
104
|
const cached = this.templateCache.get(templateId);
|
|
76
|
-
cached.
|
|
77
|
-
|
|
105
|
+
if (cached && cached.expiresAt.getTime() > Date.now()) {
|
|
106
|
+
cached.hits++;
|
|
107
|
+
this.cacheHits++;
|
|
108
|
+
// Atualiza ordem para limitar o mapa por LRU aproximado.
|
|
109
|
+
this.templateCache.delete(templateId);
|
|
110
|
+
this.templateCache.set(templateId, cached);
|
|
111
|
+
return this.copyTemplate(cached.template);
|
|
112
|
+
}
|
|
113
|
+
if (cached)
|
|
114
|
+
this.templateCache.delete(templateId);
|
|
78
115
|
}
|
|
79
|
-
|
|
80
|
-
const template = template_factory_1.TemplateFactory.createTemplate(themeType, variant,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// store em cache
|
|
84
|
-
if (this.options.cache || (options === null || options === void 0 ? void 0 : options.cache)) {
|
|
85
|
-
const ttl = ((options === null || options === void 0 ? void 0 : options.cacheTTL) || this.options.cacheTTL || this.defaultTTL) * 1000;
|
|
116
|
+
const safeConfig = cloneConfig(config);
|
|
117
|
+
const template = template_factory_1.TemplateFactory.createTemplate(themeType, variant, safeConfig);
|
|
118
|
+
const enhanced = this.enhanceTemplate(template, templateId);
|
|
119
|
+
if (useCache) {
|
|
86
120
|
this.templateCache.set(templateId, {
|
|
87
|
-
template:
|
|
121
|
+
template: enhanced,
|
|
88
122
|
createdAt: new Date(),
|
|
89
|
-
expiresAt: new Date(Date.now() +
|
|
90
|
-
hits:
|
|
123
|
+
expiresAt: new Date(Date.now() + ttlSeconds * 1000),
|
|
124
|
+
hits: 0
|
|
91
125
|
});
|
|
126
|
+
this.trimCache(options?.maxCacheEntries ?? this.options.maxCacheEntries);
|
|
92
127
|
}
|
|
93
|
-
|
|
94
|
-
this.
|
|
95
|
-
return enhancedTemplate;
|
|
128
|
+
this.addToHistory(templateId, enhanced, options);
|
|
129
|
+
return this.copyTemplate(enhanced);
|
|
96
130
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Cria um template a partir de um template existente (clone)
|
|
99
|
-
*/
|
|
100
131
|
cloneTemplate(template, modifications) {
|
|
101
|
-
const newConfig = {
|
|
102
|
-
...template.config,
|
|
103
|
-
...modifications
|
|
104
|
-
};
|
|
132
|
+
const newConfig = { ...cloneConfig(template.config), ...cloneConfig(modifications || {}) };
|
|
105
133
|
return this.createTemplate(template.theme, template.variant, newConfig);
|
|
106
134
|
}
|
|
107
|
-
/**
|
|
108
|
-
* Renderiza um template com os dados fornecidos
|
|
109
|
-
*/
|
|
110
135
|
async renderTemplate(template, data, options) {
|
|
111
136
|
try {
|
|
112
|
-
//
|
|
137
|
+
// _meta é reservado e sempre produzido internamente, nunca aceito do chamador.
|
|
113
138
|
const renderData = {
|
|
114
|
-
...data,
|
|
139
|
+
...(data && typeof data === 'object' ? data : {}),
|
|
115
140
|
_meta: {
|
|
116
|
-
isPreview:
|
|
141
|
+
isPreview: options?.preview ?? this.options.preview,
|
|
117
142
|
renderDate: new Date().toISOString(),
|
|
118
143
|
templateName: template.name,
|
|
119
144
|
theme: template.theme,
|
|
120
145
|
variant: template.variant
|
|
121
146
|
}
|
|
122
147
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (this.options.minify) {
|
|
127
|
-
html = this.minifyHtml(html);
|
|
128
|
-
}
|
|
129
|
-
console.log(`Template ${template.name} rendered successfully.`);
|
|
130
|
-
console.log(`Render metadata:`, renderData._meta);
|
|
131
|
-
console.log(html);
|
|
132
|
-
return html;
|
|
148
|
+
const html = await template.render(renderData);
|
|
149
|
+
// Minificação global removida: colapsar whitespace corrompia texto pré-formatado e <code>.
|
|
150
|
+
return this.options.minify ? this.minifyHtml(html) : html;
|
|
133
151
|
}
|
|
134
152
|
catch (error) {
|
|
135
|
-
throw new Error(`Failed to render template: ${error}
|
|
153
|
+
throw new Error(`Failed to render template: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
136
154
|
}
|
|
137
155
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Pré-visualiza um template sem enviar email
|
|
140
|
-
*/
|
|
141
156
|
async previewTemplate(themeType, variant, config, data) {
|
|
142
157
|
const template = this.createTemplate(themeType, variant, config, { cache: false });
|
|
143
158
|
return this.renderTemplate(template, data, { preview: true });
|
|
144
159
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Obtém informações sobre um tema específico
|
|
147
|
-
*/
|
|
148
160
|
getThemeInfo(themeType) {
|
|
149
161
|
const info = template_factory_1.TemplateFactory.getThemeInfo(themeType);
|
|
150
162
|
const theme = template_factory_1.TemplateFactory.getTheme(themeType);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
defaultConfig: this.getDefaultConfigForTheme(themeType, theme)
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Lista todos os temas disponíveis
|
|
159
|
-
*/
|
|
160
|
-
listThemes() {
|
|
161
|
-
return template_factory_1.TemplateFactory.listThemes();
|
|
163
|
+
if (!info || !theme)
|
|
164
|
+
throw new Error(`Theme ${themeType} not found`);
|
|
165
|
+
return { ...info, availableVariants: ['light', 'dark'], defaultConfig: this.getDefaultConfigForTheme(themeType) };
|
|
162
166
|
}
|
|
163
|
-
|
|
164
|
-
* Obtém estatísticas de uso dos templates
|
|
165
|
-
*/
|
|
167
|
+
listThemes() { return template_factory_1.TemplateFactory.listThemes(); }
|
|
166
168
|
getTemplateStats() {
|
|
167
|
-
const templatesByTheme =
|
|
168
|
-
[theme_enum_1.ThemeType.SYSTEM]: 0,
|
|
169
|
-
[theme_enum_1.ThemeType.MONOKAI]: 0,
|
|
170
|
-
[theme_enum_1.ThemeType.MODERN]: 0,
|
|
171
|
-
[theme_enum_1.ThemeType.CORPORATE]: 0,
|
|
172
|
-
[theme_enum_1.ThemeType.MINIMAL]: 0
|
|
173
|
-
};
|
|
174
|
-
let totalHits = 0;
|
|
169
|
+
const templatesByTheme = Object.fromEntries(Object.values(theme_enum_1.ThemeType).map((theme) => [theme, 0]));
|
|
175
170
|
for (const cache of this.templateCache.values()) {
|
|
176
|
-
|
|
177
|
-
templatesByTheme[theme]++;
|
|
178
|
-
totalHits += cache.hits;
|
|
171
|
+
templatesByTheme[cache.template.theme]++;
|
|
179
172
|
}
|
|
180
173
|
const totalCached = this.templateCache.size;
|
|
181
|
-
const totalInHistory = this.templateHistory.size;
|
|
182
174
|
return {
|
|
183
|
-
totalTemplates:
|
|
175
|
+
totalTemplates: this.templateHistory.size,
|
|
184
176
|
cachedTemplates: totalCached,
|
|
185
|
-
totalHits,
|
|
177
|
+
totalHits: this.cacheHits,
|
|
178
|
+
cacheRequests: this.cacheRequests,
|
|
186
179
|
templatesByTheme,
|
|
187
|
-
cacheHitRate:
|
|
180
|
+
cacheHitRate: this.cacheRequests > 0 ? (this.cacheHits / this.cacheRequests) * 100 : 0
|
|
188
181
|
};
|
|
189
182
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Limpa o cache de templates
|
|
192
|
-
*/
|
|
193
183
|
clearCache(themeType) {
|
|
194
184
|
let cleared = 0;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
cleared++;
|
|
200
|
-
}
|
|
185
|
+
for (const [id, cache] of this.templateCache.entries()) {
|
|
186
|
+
if (!themeType || cache.template.theme === themeType) {
|
|
187
|
+
this.templateCache.delete(id);
|
|
188
|
+
cleared++;
|
|
201
189
|
}
|
|
202
190
|
}
|
|
203
|
-
else {
|
|
204
|
-
cleared = this.templateCache.size;
|
|
205
|
-
this.templateCache.clear();
|
|
206
|
-
}
|
|
207
191
|
return cleared;
|
|
208
192
|
}
|
|
209
|
-
/**
|
|
210
|
-
* Obtém histórico de versões de um template
|
|
211
|
-
*/
|
|
212
193
|
getTemplateHistory(templateId) {
|
|
213
|
-
return this.templateHistory.get(templateId) || [];
|
|
194
|
+
return (this.templateHistory.get(templateId) || []).map(template => this.copyTemplate(template));
|
|
214
195
|
}
|
|
215
|
-
/**
|
|
216
|
-
* Restaura uma versão anterior do template
|
|
217
|
-
*/
|
|
218
196
|
restoreTemplateVersion(templateId, versionIndex) {
|
|
219
197
|
const history = this.templateHistory.get(templateId);
|
|
220
|
-
if (!history || versionIndex >= history.length)
|
|
198
|
+
if (!history || !Number.isInteger(versionIndex) || versionIndex < 0 || versionIndex >= history.length)
|
|
221
199
|
return null;
|
|
222
|
-
}
|
|
223
200
|
const oldTemplate = history[versionIndex];
|
|
224
201
|
return this.createTemplate(oldTemplate.theme, oldTemplate.variant, oldTemplate.config, { cache: false });
|
|
225
202
|
}
|
|
226
|
-
/**
|
|
227
|
-
|
|
228
|
-
*/
|
|
203
|
+
/** Encerrar o timer de limpeza quando a instância não for mais necessária. */
|
|
204
|
+
dispose() { clearInterval(this.cleanupTimer); }
|
|
229
205
|
validateTemplateConfig(config) {
|
|
230
|
-
var _a, _b, _c;
|
|
231
206
|
const errors = [];
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (config.header.logo.type === 'image' && !config.header.logo.imageUrl) {
|
|
235
|
-
errors.push('Logo image requires imageUrl');
|
|
236
|
-
}
|
|
237
|
-
if (config.header.logo.type === 'text' && !config.header.logo.text) {
|
|
238
|
-
errors.push('Logo text requires text content');
|
|
239
|
-
}
|
|
207
|
+
const fail = (fn) => { try {
|
|
208
|
+
fn();
|
|
240
209
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
210
|
+
catch (error) {
|
|
211
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
212
|
+
} };
|
|
213
|
+
const enumValue = (value, allowed, label) => {
|
|
214
|
+
if (value !== undefined && !allowed.includes(value))
|
|
215
|
+
errors.push(`Invalid ${label}: ${value}`);
|
|
216
|
+
};
|
|
217
|
+
if (config.language !== undefined && (typeof config.language !== 'string' || config.language.length > 63 || !SAFE_LANGUAGE_TAG.test(config.language))) {
|
|
218
|
+
errors.push('Invalid template language tag');
|
|
250
219
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (config.
|
|
254
|
-
errors.push(
|
|
220
|
+
enumValue(config.direction, ['ltr', 'rtl', 'auto'], 'template text direction');
|
|
221
|
+
enumValue(config.linkDecoration, ['none', 'underline'], 'template linkDecoration');
|
|
222
|
+
if (config.preheader !== undefined && (typeof config.preheader !== 'string' || config.preheader.length > 500 || /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(config.preheader))) {
|
|
223
|
+
errors.push('preheader must be a string of at most 500 printable characters');
|
|
255
224
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
225
|
+
enumValue(config.layout, ['full', 'minimal'], 'layout');
|
|
226
|
+
enumValue(config.spacing, ['compact', 'normal', 'relaxed'], 'spacing');
|
|
227
|
+
enumValue(config.borderRadius, ['none', 'small', 'medium', 'large'], 'borderRadius');
|
|
228
|
+
if (config.header) {
|
|
229
|
+
if (typeof config.header.show !== 'boolean')
|
|
230
|
+
errors.push('header.show must be a boolean');
|
|
231
|
+
if (config.header.logo) {
|
|
232
|
+
enumValue(config.header.logo.type, ['text', 'image'], 'header.logo.type');
|
|
233
|
+
enumValue(config.header.logo.size, ['small', 'medium', 'large'], 'header.logo.size');
|
|
234
|
+
if (config.header.logo.type === 'image' && !hasTemplatePlaceholder(config.header.logo.imageUrl))
|
|
235
|
+
fail(() => assertSafeUrl(config.header.logo.imageUrl, false, 'header.logo.imageUrl'));
|
|
236
|
+
if (config.header.logo.type === 'text' && typeof config.header.logo.text !== 'string')
|
|
237
|
+
errors.push('header.logo.text must be a string');
|
|
238
|
+
}
|
|
239
|
+
if (config.header.backgroundColor !== undefined && !isSafeColor(config.header.backgroundColor))
|
|
240
|
+
errors.push('Invalid header.backgroundColor');
|
|
241
|
+
if (config.header.textColor !== undefined && !isSafeColor(config.header.textColor))
|
|
242
|
+
errors.push('Invalid header.textColor');
|
|
260
243
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
244
|
+
const body = config.body;
|
|
245
|
+
if (body) {
|
|
246
|
+
enumValue(body.alignment, ['left', 'center', 'right'], 'body.alignment');
|
|
247
|
+
enumValue(body.buttonVariant, ['primary', 'secondary', 'success', 'danger'], 'body.buttonVariant');
|
|
248
|
+
if (body.fontSize !== undefined && (!Number.isFinite(body.fontSize) || body.fontSize < 8 || body.fontSize > 72))
|
|
249
|
+
errors.push('body.fontSize must be between 8 and 72');
|
|
250
|
+
for (const key of ['backgroundColor', 'textColor']) {
|
|
251
|
+
const color = body[key];
|
|
252
|
+
if (color !== undefined && !isSafeColor(color))
|
|
253
|
+
errors.push(`Invalid body.${key}`);
|
|
254
|
+
}
|
|
255
|
+
const buttonUrl = body.buttonUrl;
|
|
256
|
+
if (buttonUrl !== undefined && !hasTemplatePlaceholder(buttonUrl))
|
|
257
|
+
fail(() => assertSafeUrl(buttonUrl, true, 'body.buttonUrl'));
|
|
258
|
+
for (const key of ['title', 'message', 'content', 'buttonText']) {
|
|
259
|
+
if (body[key] !== undefined && typeof body[key] !== 'string')
|
|
260
|
+
errors.push(`body.${key} must be a string`);
|
|
261
|
+
}
|
|
265
262
|
}
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
const footer = config.footer;
|
|
264
|
+
if (footer) {
|
|
265
|
+
if (typeof footer.show !== 'boolean')
|
|
266
|
+
errors.push('footer.show must be a boolean');
|
|
267
|
+
for (const key of ['backgroundColor', 'textColor']) {
|
|
268
|
+
const color = footer[key];
|
|
269
|
+
if (color !== undefined && !isSafeColor(color))
|
|
270
|
+
errors.push(`Invalid footer.${key}`);
|
|
271
|
+
}
|
|
272
|
+
for (const link of footer.links || []) {
|
|
273
|
+
if (!link || typeof link.text !== 'string')
|
|
274
|
+
errors.push('Footer links require text and URL');
|
|
275
|
+
else if (!hasTemplatePlaceholder(link.url))
|
|
276
|
+
fail(() => assertSafeUrl(link.url, true, 'footer link URL'));
|
|
277
|
+
}
|
|
278
|
+
for (const link of footer.socialLinks || []) {
|
|
279
|
+
if (!link || !SOCIAL_PLATFORMS.has(link.platform))
|
|
280
|
+
errors.push(`Invalid social platform: ${link?.platform}`);
|
|
281
|
+
else if (!hasTemplatePlaceholder(link.url))
|
|
282
|
+
fail(() => assertSafeUrl(link.url, false, 'social link URL'));
|
|
283
|
+
}
|
|
284
|
+
const unsubscribeUrl = footer.unsubscribeUrl;
|
|
285
|
+
if (unsubscribeUrl !== undefined && !hasTemplatePlaceholder(unsubscribeUrl))
|
|
286
|
+
fail(() => assertSafeUrl(unsubscribeUrl, true, 'footer.unsubscribeUrl'));
|
|
268
287
|
}
|
|
288
|
+
if (errors.length)
|
|
289
|
+
throw new Error(`Template configuration validation failed:\n${errors.join('\n')}`);
|
|
290
|
+
}
|
|
291
|
+
assertThemeAndVariant(themeType, variant) {
|
|
292
|
+
if (!VALID_THEMES.includes(themeType))
|
|
293
|
+
throw new Error(`Invalid theme: ${themeType}`);
|
|
294
|
+
if (variant !== 'light' && variant !== 'dark')
|
|
295
|
+
throw new Error(`Invalid template variant: ${variant}`);
|
|
269
296
|
}
|
|
270
|
-
/**
|
|
271
|
-
* Gera um ID único para o template baseado na configuração
|
|
272
|
-
*/
|
|
273
297
|
generateTemplateId(themeType, variant, config) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
variant,
|
|
277
|
-
config
|
|
278
|
-
});
|
|
279
|
-
// Hash simples para ID
|
|
280
|
-
let hash = 0;
|
|
281
|
-
for (let i = 0; i < configString.length; i++) {
|
|
282
|
-
const char = configString.charCodeAt(i);
|
|
283
|
-
hash = ((hash << 5) - hash) + char;
|
|
284
|
-
hash = hash & hash;
|
|
285
|
-
}
|
|
286
|
-
return `${themeType}_${variant}_${Math.abs(hash)}`;
|
|
298
|
+
const canonical = JSON.stringify(canonicalize({ theme: themeType, variant, config }));
|
|
299
|
+
return `${themeType}_${variant}_${(0, node_crypto_1.createHash)('sha256').update(canonical).digest('hex')}`;
|
|
287
300
|
}
|
|
288
|
-
/**
|
|
289
|
-
* Adiciona funcionalidades extras ao template
|
|
290
|
-
*/
|
|
291
301
|
enhanceTemplate(template, templateId) {
|
|
292
302
|
const enhanced = { ...template };
|
|
293
|
-
|
|
294
|
-
enhanced.getVersion = () => {
|
|
295
|
-
const history = this.templateHistory.get(templateId);
|
|
296
|
-
return history ? history.length : 1;
|
|
297
|
-
};
|
|
298
|
-
// add método para obter ID
|
|
303
|
+
enhanced.getVersion = () => this.templateHistory.get(templateId)?.length || 1;
|
|
299
304
|
enhanced.getId = () => templateId;
|
|
300
|
-
|
|
301
|
-
enhanced.clone = (modifications) => {
|
|
302
|
-
return this.cloneTemplate(enhanced, modifications);
|
|
303
|
-
};
|
|
305
|
+
enhanced.clone = (modifications) => this.cloneTemplate(enhanced, modifications);
|
|
304
306
|
return enhanced;
|
|
305
307
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
addToHistory(templateId, template) {
|
|
310
|
-
|
|
311
|
-
this.templateHistory.set(templateId, []);
|
|
312
|
-
}
|
|
313
|
-
const history = this.templateHistory.get(templateId);
|
|
308
|
+
copyTemplate(template) {
|
|
309
|
+
return { ...template, config: cloneConfig(template.config) };
|
|
310
|
+
}
|
|
311
|
+
addToHistory(templateId, template, options) {
|
|
312
|
+
const history = this.templateHistory.get(templateId) || [];
|
|
314
313
|
history.push(template);
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
const perTemplateLimit = options?.maxVersionsPerTemplate ?? this.options.maxVersionsPerTemplate;
|
|
315
|
+
while (history.length > perTemplateLimit)
|
|
317
316
|
history.shift();
|
|
317
|
+
this.templateHistory.delete(templateId);
|
|
318
|
+
this.templateHistory.set(templateId, history);
|
|
319
|
+
const historyLimit = options?.maxHistoryEntries ?? this.options.maxHistoryEntries;
|
|
320
|
+
while (this.templateHistory.size > historyLimit) {
|
|
321
|
+
const oldest = this.templateHistory.keys().next().value;
|
|
322
|
+
if (oldest === undefined)
|
|
323
|
+
break;
|
|
324
|
+
this.templateHistory.delete(oldest);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
trimCache(limit) {
|
|
328
|
+
while (this.templateCache.size > limit) {
|
|
329
|
+
const oldest = this.templateCache.keys().next().value;
|
|
330
|
+
if (oldest === undefined)
|
|
331
|
+
break;
|
|
332
|
+
this.templateCache.delete(oldest);
|
|
318
333
|
}
|
|
319
334
|
}
|
|
320
|
-
/**
|
|
321
|
-
* Remove templates expirados do cache
|
|
322
|
-
*/
|
|
323
335
|
cleanExpiredCache() {
|
|
324
336
|
const now = Date.now();
|
|
325
337
|
for (const [id, cache] of this.templateCache.entries()) {
|
|
326
|
-
if (cache.expiresAt.getTime()
|
|
338
|
+
if (cache.expiresAt.getTime() <= now)
|
|
327
339
|
this.templateCache.delete(id);
|
|
328
|
-
}
|
|
329
340
|
}
|
|
330
341
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
.replace(/>\s+</g, '><')
|
|
338
|
-
.replace(/<!--.*?-->/g, '')
|
|
339
|
-
.trim();
|
|
342
|
+
// Mantido como transformação deliberadamente não destrutiva. HTML minification baseada em regex
|
|
343
|
+
// alteraria espaços de texto e conteúdo de <code>/<pre>; minify=true preserva o HTML original.
|
|
344
|
+
minifyHtml(html) { return html; }
|
|
345
|
+
validateLimit(value, name) {
|
|
346
|
+
if (!Number.isInteger(value) || value < 1)
|
|
347
|
+
throw new Error(`${name} must be a positive integer`);
|
|
340
348
|
}
|
|
341
|
-
|
|
342
|
-
* Obtém configuração padrão para um tema
|
|
343
|
-
*/
|
|
344
|
-
getDefaultConfigForTheme(themeType, theme) {
|
|
349
|
+
getDefaultConfigForTheme(themeType) {
|
|
345
350
|
const baseConfig = {
|
|
346
|
-
header: {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
type: 'text',
|
|
350
|
-
text: 'MyApp',
|
|
351
|
-
size: 'medium'
|
|
352
|
-
}
|
|
353
|
-
},
|
|
354
|
-
footer: {
|
|
355
|
-
show: true,
|
|
356
|
-
copyrightText: `© ${new Date().getFullYear()} MyApp. All rights reserved.`
|
|
357
|
-
},
|
|
358
|
-
layout: 'full',
|
|
359
|
-
spacing: 'normal',
|
|
360
|
-
borderRadius: 'medium'
|
|
351
|
+
header: { show: true, logo: { type: 'text', text: 'MyApp', size: 'medium' } },
|
|
352
|
+
footer: { show: true, copyrightText: `© ${new Date().getFullYear()} MyApp. All rights reserved.` },
|
|
353
|
+
layout: 'full', spacing: 'normal', borderRadius: 'medium'
|
|
361
354
|
};
|
|
362
|
-
// Ajustes específicos por tema
|
|
363
355
|
switch (themeType) {
|
|
364
|
-
case theme_enum_1.ThemeType.MONOKAI:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
};
|
|
370
|
-
case theme_enum_1.ThemeType.MODERN:
|
|
371
|
-
return {
|
|
372
|
-
...baseConfig,
|
|
373
|
-
borderRadius: 'large',
|
|
374
|
-
spacing: 'relaxed'
|
|
375
|
-
};
|
|
376
|
-
case theme_enum_1.ThemeType.CORPORATE:
|
|
377
|
-
return {
|
|
378
|
-
...baseConfig,
|
|
379
|
-
borderRadius: 'small',
|
|
380
|
-
spacing: 'normal'
|
|
381
|
-
};
|
|
382
|
-
case theme_enum_1.ThemeType.MINIMAL:
|
|
383
|
-
return {
|
|
384
|
-
...baseConfig,
|
|
385
|
-
borderRadius: 'none',
|
|
386
|
-
spacing: 'relaxed'
|
|
387
|
-
};
|
|
388
|
-
default:
|
|
389
|
-
return baseConfig;
|
|
356
|
+
case theme_enum_1.ThemeType.MONOKAI: return { ...baseConfig, borderRadius: 'small' };
|
|
357
|
+
case theme_enum_1.ThemeType.MODERN: return { ...baseConfig, borderRadius: 'large', spacing: 'relaxed' };
|
|
358
|
+
case theme_enum_1.ThemeType.CORPORATE: return { ...baseConfig, borderRadius: 'small' };
|
|
359
|
+
case theme_enum_1.ThemeType.MINIMAL: return { ...baseConfig, borderRadius: 'none', spacing: 'relaxed' };
|
|
360
|
+
default: return baseConfig;
|
|
390
361
|
}
|
|
391
362
|
}
|
|
392
|
-
/**
|
|
393
|
-
* Pré-carrega templates no cache
|
|
394
|
-
*/
|
|
395
363
|
async preloadTemplates(templates) {
|
|
396
|
-
|
|
397
|
-
this.createTemplate(themeType, variant, config);
|
|
398
|
-
});
|
|
399
|
-
await Promise.all(promises);
|
|
364
|
+
await Promise.all(templates.map(({ themeType, variant, config }) => this.createTemplate(themeType, variant, config)));
|
|
400
365
|
}
|
|
401
|
-
/**
|
|
402
|
-
* Exporta template para formato JSON
|
|
403
|
-
*/
|
|
404
366
|
exportTemplate(template) {
|
|
405
|
-
|
|
406
|
-
return JSON.stringify({
|
|
407
|
-
name: template.name,
|
|
408
|
-
theme: template.theme,
|
|
409
|
-
variant: template.variant,
|
|
410
|
-
config: template.config,
|
|
411
|
-
exportedAt: new Date().toISOString(),
|
|
412
|
-
version: ((_b = (_a = template).getVersion) === null || _b === void 0 ? void 0 : _b.call(_a)) || 1
|
|
413
|
-
}, null, 2);
|
|
367
|
+
return JSON.stringify({ name: template.name, theme: template.theme, variant: template.variant, config: template.config, exportedAt: new Date().toISOString(), version: template.getVersion?.() || 1 }, null, 2);
|
|
414
368
|
}
|
|
415
|
-
/**
|
|
416
|
-
* Importa template de formato JSON
|
|
417
|
-
*/
|
|
418
369
|
importTemplate(jsonData) {
|
|
419
370
|
try {
|
|
420
371
|
const data = JSON.parse(jsonData);
|
|
421
|
-
if (!data.theme || !data.variant || !data.config) {
|
|
422
|
-
throw new Error('Invalid template data:
|
|
372
|
+
if (!data || typeof data !== 'object' || !VALID_THEMES.includes(data.theme) || !['light', 'dark'].includes(data.variant) || !data.config || typeof data.config !== 'object' || Array.isArray(data.config)) {
|
|
373
|
+
throw new Error('Invalid template data: theme, variant, and config are required and must be valid');
|
|
423
374
|
}
|
|
424
375
|
return this.createTemplate(data.theme, data.variant, data.config);
|
|
425
376
|
}
|
|
426
377
|
catch (error) {
|
|
427
|
-
throw new Error(`Failed to import template: ${error}
|
|
378
|
+
throw new Error(`Failed to import template: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
428
379
|
}
|
|
429
380
|
}
|
|
430
|
-
/**
|
|
431
|
-
* Gera um template de exemplo para demonstração
|
|
432
|
-
*/
|
|
433
381
|
generateExampleTemplate(themeType = theme_enum_1.ThemeType.MODERN, variant = 'light') {
|
|
434
382
|
const exampleConfig = {
|
|
435
|
-
header: {
|
|
436
|
-
show: true,
|
|
437
|
-
logo: {
|
|
438
|
-
type: 'text',
|
|
439
|
-
text: 'ExampleApp',
|
|
440
|
-
size: 'medium'
|
|
441
|
-
},
|
|
442
|
-
backgroundColor: variant === 'light' ? '#ffffff' : '#1a1a1a',
|
|
443
|
-
textColor: variant === 'light' ? '#111827' : '#f9fafb'
|
|
444
|
-
},
|
|
383
|
+
header: { show: true, logo: { type: 'text', text: 'ExampleApp', size: 'medium' }, backgroundColor: variant === 'light' ? '#ffffff' : '#1a1a1a', textColor: variant === 'light' ? '#111827' : '#f9fafb' },
|
|
445
384
|
footer: {
|
|
446
385
|
show: true,
|
|
447
|
-
links: [
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
],
|
|
452
|
-
socialLinks: [
|
|
453
|
-
{ platform: 'twitter', url: 'https://twitter.com/example' },
|
|
454
|
-
{ platform: 'github', url: 'https://github.com/example' },
|
|
455
|
-
{ platform: 'linkedin', url: 'https://linkedin.com/company/example' }
|
|
456
|
-
],
|
|
457
|
-
copyrightText: `© ${new Date().getFullYear()} ExampleApp. All rights reserved.`,
|
|
458
|
-
unsubscribeText: 'Unsubscribe',
|
|
459
|
-
backgroundColor: variant === 'light' ? '#f9fafb' : '#111827',
|
|
460
|
-
textColor: variant === 'light' ? '#6b7280' : '#9ca3af'
|
|
386
|
+
links: [{ text: 'Privacy Policy', url: 'https://example.com/privacy' }, { text: 'Terms of Service', url: 'https://example.com/terms' }, { text: 'Contact', url: 'https://example.com/contact' }],
|
|
387
|
+
socialLinks: [{ platform: 'twitter', url: 'https://twitter.com/example' }, { platform: 'github', url: 'https://github.com/example' }, { platform: 'linkedin', url: 'https://linkedin.com/company/example' }],
|
|
388
|
+
copyrightText: `© ${new Date().getFullYear()} ExampleApp. All rights reserved.`, unsubscribeText: 'Unsubscribe',
|
|
389
|
+
backgroundColor: variant === 'light' ? '#f9fafb' : '#111827', textColor: variant === 'light' ? '#6b7280' : '#9ca3af'
|
|
461
390
|
},
|
|
462
|
-
layout: 'full',
|
|
463
|
-
spacing: 'normal',
|
|
464
|
-
borderRadius: themeType === theme_enum_1.ThemeType.MINIMAL ? 'none' : 'medium'
|
|
391
|
+
layout: 'full', spacing: 'normal', borderRadius: themeType === theme_enum_1.ThemeType.MINIMAL ? 'none' : 'medium'
|
|
465
392
|
};
|
|
466
393
|
return this.createTemplate(themeType, variant, exampleConfig);
|
|
467
394
|
}
|
|
468
|
-
/**
|
|
469
|
-
* Valida se um template é válido
|
|
470
|
-
*/
|
|
471
395
|
isValidTemplate(template) {
|
|
472
396
|
try {
|
|
473
|
-
|
|
474
|
-
if (!template || typeof template !== 'object')
|
|
475
|
-
return false;
|
|
476
|
-
if (!template.name || typeof template.name !== 'string')
|
|
477
|
-
return false;
|
|
478
|
-
if (!template.theme || !Object.values(theme_enum_1.ThemeType).includes(template.theme))
|
|
479
|
-
return false;
|
|
480
|
-
if (!template.variant || !['light', 'dark'].includes(template.variant))
|
|
481
|
-
return false;
|
|
482
|
-
if (!template.config || typeof template.config !== 'object')
|
|
483
|
-
return false;
|
|
484
|
-
if (typeof template.render !== 'function')
|
|
397
|
+
if (!template || typeof template !== 'object' || typeof template.name !== 'string' || !VALID_THEMES.includes(template.theme) || !['light', 'dark'].includes(template.variant) || !template.config || typeof template.config !== 'object' || typeof template.render !== 'function')
|
|
485
398
|
return false;
|
|
399
|
+
this.validateTemplateConfig(template.config);
|
|
486
400
|
return true;
|
|
487
401
|
}
|
|
488
402
|
catch {
|
|
489
403
|
return false;
|
|
490
404
|
}
|
|
491
405
|
}
|
|
492
|
-
/**
|
|
493
|
-
* Obtém estatísticas detalhadas de um template específico
|
|
494
|
-
*/
|
|
495
406
|
getTemplateDetails(templateId) {
|
|
496
407
|
const cacheInfo = this.templateCache.get(templateId) || null;
|
|
497
|
-
const history = this.templateHistory.get(templateId) || [];
|
|
498
|
-
const
|
|
408
|
+
const history = (this.templateHistory.get(templateId) || []).map(template => this.copyTemplate(template));
|
|
409
|
+
const template = cacheInfo?.template || history[history.length - 1] || null;
|
|
499
410
|
return {
|
|
500
|
-
template:
|
|
411
|
+
template: template ? this.copyTemplate(template) : null,
|
|
501
412
|
history,
|
|
502
|
-
cacheInfo,
|
|
503
|
-
usageCount
|
|
413
|
+
cacheInfo: cacheInfo ? { ...cacheInfo, template: this.copyTemplate(cacheInfo.template), createdAt: new Date(cacheInfo.createdAt), expiresAt: new Date(cacheInfo.expiresAt) } : null,
|
|
414
|
+
usageCount: cacheInfo?.hits || 0
|
|
504
415
|
};
|
|
505
416
|
}
|
|
506
417
|
}
|