@fufulog/brevomorphic-cms-sdk 1.0.5 → 1.0.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.
package/dist/index.d.ts CHANGED
@@ -3,3 +3,4 @@ export { EmailTemplateRepository } from './repository.js';
3
3
  export { EmailTemplateService } from './service.js';
4
4
  export { EmailTemplateController } from './controller.js';
5
5
  export * from './types.js';
6
+ export { restoreTwigTags } from './twig.js';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export { EmailTemplateRepository } from './repository.js';
3
3
  export { EmailTemplateService } from './service.js';
4
4
  export { EmailTemplateController } from './controller.js';
5
5
  export * from './types.js';
6
+ export { restoreTwigTags } from './twig.js';
package/dist/service.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BrevoClient } from './brevoClient.js';
2
+ import { restoreTwigTags } from './twig.js';
2
3
  import { EmailTemplateRepository } from './repository.js';
3
4
  export class EmailTemplateService {
4
5
  brevoClient;
@@ -108,6 +109,8 @@ export class EmailTemplateService {
108
109
  if (!input.htmlContent || input.htmlContent.length <= 10) {
109
110
  throw new Error('HTML content must be greater than 10 characters');
110
111
  }
112
+ // Editors that read innerHTML send Twig operators as entities (&gt;), which Brevo rejects
113
+ const htmlContent = restoreTwigTags(input.htmlContent);
111
114
  const sender = {
112
115
  email: input.sender?.email || this.defaultSender.email,
113
116
  name: input.sender?.name || this.defaultSender.name || '',
@@ -117,7 +120,7 @@ export class EmailTemplateService {
117
120
  templateName: input.templateName,
118
121
  subject: input.subject,
119
122
  sender: sender,
120
- htmlContent: input.htmlContent,
123
+ htmlContent,
121
124
  isActive: input.isActive,
122
125
  });
123
126
  // 2. Update local mapping DB
@@ -129,7 +132,7 @@ export class EmailTemplateService {
129
132
  eventName: input.eventName || '',
130
133
  isActive: input.isActive,
131
134
  sender: sender,
132
- htmlContent: input.htmlContent,
135
+ htmlContent,
133
136
  };
134
137
  }
135
138
  /**
@@ -143,7 +146,7 @@ export class EmailTemplateService {
143
146
  templateName: bt.name,
144
147
  subject: bt.subject,
145
148
  sender: bt.sender || this.defaultSender,
146
- htmlContent: bt.htmlContent || '',
149
+ htmlContent: restoreTwigTags(bt.htmlContent || ''),
147
150
  isActive: isActive,
148
151
  });
149
152
  // 3. Update local DB mapping status
package/dist/twig.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Undo HTML entity encoding that breaks Brevo's Twig parser.
3
+ *
4
+ * Reading a contentEditable element's innerHTML escapes text, so a condition like
5
+ * `{% if params.count > 1 %}` comes back as `{% if params.count &gt; 1 %}` and Brevo
6
+ * rejects it as a malformed if-condition. Brackets are restored everywhere; operators,
7
+ * quotes and ampersands only inside `{% ... %}` and `{{ ... }}` tags, so the rest of
8
+ * the HTML keeps its escaping.
9
+ */
10
+ export declare function restoreTwigTags(html: string): string;
package/dist/twig.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Undo HTML entity encoding that breaks Brevo's Twig parser.
3
+ *
4
+ * Reading a contentEditable element's innerHTML escapes text, so a condition like
5
+ * `{% if params.count > 1 %}` comes back as `{% if params.count &gt; 1 %}` and Brevo
6
+ * rejects it as a malformed if-condition. Brackets are restored everywhere; operators,
7
+ * quotes and ampersands only inside `{% ... %}` and `{{ ... }}` tags, so the rest of
8
+ * the HTML keeps its escaping.
9
+ */
10
+ export function restoreTwigTags(html) {
11
+ return html
12
+ .replace(/&lbrace;/g, '{')
13
+ .replace(/&rbrace;/g, '}')
14
+ .replace(/&#123;/g, '{')
15
+ .replace(/&#125;/g, '}')
16
+ .replace(/&#37;/g, '%')
17
+ .replace(/&percnt;/g, '%')
18
+ .replace(/\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\}/g, (tag) => tag
19
+ .replace(/&gt;/g, '>')
20
+ .replace(/&lt;/g, '<')
21
+ .replace(/&quot;/g, '"')
22
+ .replace(/&#39;/g, "'")
23
+ .replace(/&nbsp;/g, ' ')
24
+ .replace(/&amp;/g, '&'));
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fufulog/brevomorphic-cms-sdk",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "description": "SDK library to bootstrap Brevo email templates and manage local mappings in Postgres, MySQL, or Firestore",
6
6
  "main": "dist/index.js",
@@ -85,7 +85,17 @@ export default function BrevoWysiwyg({
85
85
  .replace(/&#123;/g, '{')
86
86
  .replace(/&#125;/g, '}')
87
87
  .replace(/&#37;/g, '%')
88
- .replace(/&percnt;/g, '%');
88
+ .replace(/&percnt;/g, '%')
89
+ // innerHTML escapes > < & and quotes in text, which breaks Twig conditions like {% if x > 1 %}
90
+ .replace(/\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\}/g, (tag) =>
91
+ tag
92
+ .replace(/&gt;/g, '>')
93
+ .replace(/&lt;/g, '<')
94
+ .replace(/&quot;/g, '"')
95
+ .replace(/&#39;/g, "'")
96
+ .replace(/&nbsp;/g, ' ')
97
+ .replace(/&amp;/g, '&')
98
+ );
89
99
  }, []);
90
100
 
91
101
  const syncFromEditor = useCallback(() => {
package/src/index.ts CHANGED
@@ -3,3 +3,4 @@ export { EmailTemplateRepository } from './repository.js';
3
3
  export { EmailTemplateService } from './service.js';
4
4
  export { EmailTemplateController } from './controller.js';
5
5
  export * from './types.js';
6
+ export { restoreTwigTags } from './twig.js';
package/src/service.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BrevoClient } from './brevoClient.js';
2
+ import { restoreTwigTags } from './twig.js';
2
3
  import { EmailTemplateRepository } from './repository.js';
3
4
  import { SDKConfig, CombinedTemplate, CreateTemplateInput, UpdateTemplateInput, BrevoSender } from './types.js';
4
5
 
@@ -129,6 +130,9 @@ export class EmailTemplateService {
129
130
  throw new Error('HTML content must be greater than 10 characters');
130
131
  }
131
132
 
133
+ // Editors that read innerHTML send Twig operators as entities (&gt;), which Brevo rejects
134
+ const htmlContent = restoreTwigTags(input.htmlContent);
135
+
132
136
  const sender: BrevoSender = {
133
137
  email: input.sender?.email || this.defaultSender.email,
134
138
  name: input.sender?.name || this.defaultSender.name || '',
@@ -139,7 +143,7 @@ export class EmailTemplateService {
139
143
  templateName: input.templateName,
140
144
  subject: input.subject,
141
145
  sender: sender,
142
- htmlContent: input.htmlContent,
146
+ htmlContent,
143
147
  isActive: input.isActive,
144
148
  });
145
149
 
@@ -153,7 +157,7 @@ export class EmailTemplateService {
153
157
  eventName: input.eventName || '',
154
158
  isActive: input.isActive,
155
159
  sender: sender,
156
- htmlContent: input.htmlContent,
160
+ htmlContent,
157
161
  };
158
162
  }
159
163
 
@@ -169,7 +173,7 @@ export class EmailTemplateService {
169
173
  templateName: bt.name,
170
174
  subject: bt.subject,
171
175
  sender: bt.sender || this.defaultSender,
172
- htmlContent: bt.htmlContent || '',
176
+ htmlContent: restoreTwigTags(bt.htmlContent || ''),
173
177
  isActive: isActive,
174
178
  });
175
179
 
package/src/twig.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Undo HTML entity encoding that breaks Brevo's Twig parser.
3
+ *
4
+ * Reading a contentEditable element's innerHTML escapes text, so a condition like
5
+ * `{% if params.count > 1 %}` comes back as `{% if params.count &gt; 1 %}` and Brevo
6
+ * rejects it as a malformed if-condition. Brackets are restored everywhere; operators,
7
+ * quotes and ampersands only inside `{% ... %}` and `{{ ... }}` tags, so the rest of
8
+ * the HTML keeps its escaping.
9
+ */
10
+ export function restoreTwigTags(html: string): string {
11
+ return html
12
+ .replace(/&lbrace;/g, '{')
13
+ .replace(/&rbrace;/g, '}')
14
+ .replace(/&#123;/g, '{')
15
+ .replace(/&#125;/g, '}')
16
+ .replace(/&#37;/g, '%')
17
+ .replace(/&percnt;/g, '%')
18
+ .replace(/\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\}/g, (tag) =>
19
+ tag
20
+ .replace(/&gt;/g, '>')
21
+ .replace(/&lt;/g, '<')
22
+ .replace(/&quot;/g, '"')
23
+ .replace(/&#39;/g, "'")
24
+ .replace(/&nbsp;/g, ' ')
25
+ .replace(/&amp;/g, '&')
26
+ );
27
+ }
@@ -70,7 +70,17 @@
70
70
  .replace(/&#123;/g, "{")
71
71
  .replace(/&#125;/g, "}")
72
72
  .replace(/&#37;/g, "%")
73
- .replace(/&percnt;/g, "%");
73
+ .replace(/&percnt;/g, "%")
74
+ // innerHTML escapes > < & and quotes in text, which breaks Twig conditions like {% if x > 1 %}
75
+ .replace(/\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\}/g, (tag) =>
76
+ tag
77
+ .replace(/&gt;/g, ">")
78
+ .replace(/&lt;/g, "<")
79
+ .replace(/&quot;/g, '"')
80
+ .replace(/&#39;/g, "'")
81
+ .replace(/&nbsp;/g, " ")
82
+ .replace(/&amp;/g, "&")
83
+ );
74
84
  value = cleaned;
75
85
  dispatch("change", cleaned);
76
86
  }
package/tests/sdk.test.ts CHANGED
@@ -2,6 +2,7 @@ import { EmailTemplateRepository } from '../src/repository.js';
2
2
  import { EmailTemplateService } from '../src/service.js';
3
3
  import { EmailTemplateController } from '../src/controller.js';
4
4
  import { SQLClient, LocalMapping } from '../src/types.js';
5
+ import { restoreTwigTags } from '../src/twig.js';
5
6
 
6
7
  // Setup basic assertion helper
7
8
  function assert(condition: boolean, message: string) {
@@ -221,7 +222,43 @@ async function runTests() {
221
222
  }
222
223
  });
223
224
 
224
- // 5. CONTROLLER TESTS
225
+ // 5. TWIG ENTITY RESTORE TESTS
226
+ runTest('restoreTwigTags - Decodes operators inside Twig tags only', () => {
227
+ const input = '<p>A &gt; B &amp; C</p>{% if params.count &gt; 1 %}x{% endif %}{{ params.a &amp;&amp; params.b }}{% if params.name == &quot;Jo&quot; %}y{% endif %}';
228
+ const output = restoreTwigTags(input);
229
+ assert(output.includes('<p>A &gt; B &amp; C</p>'), 'HTML text outside tags keeps its escaping');
230
+ assert(output.includes('{% if params.count > 1 %}'), 'Comparison operator is restored');
231
+ assert(output.includes('{{ params.a && params.b }}'), 'Ampersands are restored inside output tags');
232
+ assert(output.includes('{% if params.name == "Jo" %}'), 'Quotes are restored');
233
+ });
234
+
235
+ runTest('restoreTwigTags - Restores encoded brackets', () => {
236
+ assert(restoreTwigTags('&#123;&#123; params.x &#125;&#125;') === '{{ params.x }}', 'Numeric brace entities are restored');
237
+ assert(restoreTwigTags('&lbrace;&#37; if a &gt; 1 &#37;&rbrace;') === '{% if a > 1 %}', 'Encoded tag delimiters and operators are restored');
238
+ });
239
+
240
+ runTest('Service - updateTemplate sends restored Twig to Brevo', async () => {
241
+ const mockPgClient: SQLClient = { query: async () => ({ rows: [] }) };
242
+ const service = new EmailTemplateService({
243
+ brevoApiKey: 'dummy-key',
244
+ defaultSender: { email: 'test@example.com' },
245
+ dbType: 'postgres',
246
+ dbClient: mockPgClient
247
+ });
248
+ let sentHtml = '';
249
+ (service as any).brevoClient = { updateTemplate: async (_id: number, params: { htmlContent: string }) => { sentHtml = params.htmlContent; } };
250
+ const result = await service.updateTemplate(7, {
251
+ templateName: 'Invite',
252
+ subject: 'You have a ticket',
253
+ htmlContent: '<h1>{% if params.ticketCount &gt; 1 %}Tickets{% else %}Ticket{% endif %}</h1>',
254
+ eventName: 'ticket.invite',
255
+ isActive: true
256
+ });
257
+ assert(sentHtml === '<h1>{% if params.ticketCount > 1 %}Tickets{% else %}Ticket{% endif %}</h1>', 'Brevo receives the decoded condition');
258
+ assert(result.htmlContent === sentHtml, 'Returned template matches what was sent');
259
+ });
260
+
261
+ // 6. CONTROLLER TESTS
225
262
  runTest('Controller - getRouter with Express', async () => {
226
263
  const mockPgClient: SQLClient = { query: async () => ({ rows: [] }) };
227
264
  const service = new EmailTemplateService({