@fufulog/brevomorphic-cms-sdk 1.0.4 โ†’ 1.0.5

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.
@@ -0,0 +1,239 @@
1
+ import { EmailTemplateRepository } from '../src/repository.js';
2
+ import { EmailTemplateService } from '../src/service.js';
3
+ import { EmailTemplateController } from '../src/controller.js';
4
+ import { SQLClient, LocalMapping } from '../src/types.js';
5
+
6
+ // Setup basic assertion helper
7
+ function assert(condition: boolean, message: string) {
8
+ if (!condition) {
9
+ throw new Error(`Assertion Failed: ${message}`);
10
+ }
11
+ }
12
+
13
+ async function runTests() {
14
+ console.log('๐Ÿงช Starting Brevo CMS SDK Unit Tests...\n');
15
+
16
+ let testPassedCount = 0;
17
+ let testFailedCount = 0;
18
+
19
+ function runTest(name: string, testFn: () => void | Promise<void>) {
20
+ try {
21
+ const p = testFn();
22
+ if (p instanceof Promise) {
23
+ p.then(() => {
24
+ console.log(`โœ… PASSED: ${name}`);
25
+ testPassedCount++;
26
+ }).catch((err) => {
27
+ console.error(`โŒ FAILED: ${name}`);
28
+ console.error(err);
29
+ testFailedCount++;
30
+ });
31
+ } else {
32
+ console.log(`โœ… PASSED: ${name}`);
33
+ testPassedCount++;
34
+ }
35
+ } catch (err) {
36
+ console.error(`โŒ FAILED: ${name}`);
37
+ console.error(err);
38
+ testFailedCount++;
39
+ }
40
+ }
41
+
42
+ // 1. REPOSITORY TESTS (SQL PostgreSQL Dialect)
43
+ runTest('Repository - PostgreSQL Queries', async () => {
44
+ const executedQueries: { sql: string; params: any[] }[] = [];
45
+ const mockPgClient: SQLClient = {
46
+ query: async (sql: string, params: any[]) => {
47
+ executedQueries.push({ sql, params });
48
+ // return standard PG rows layout
49
+ return {
50
+ rows: [
51
+ { id: 1, template_id: 100, event_name: 'test.event', is_active: true, updated_at: new Date() }
52
+ ]
53
+ };
54
+ }
55
+ };
56
+
57
+ const repo = new EmailTemplateRepository({
58
+ brevoApiKey: 'test-key',
59
+ defaultSender: { email: 'test@example.com' },
60
+ dbType: 'postgres',
61
+ dbClient: mockPgClient
62
+ });
63
+
64
+ const mapping = await repo.getMappingByTemplateId(100);
65
+ assert(mapping !== null, 'Should return parsed mapping');
66
+ assert(mapping?.template_id === 100, 'Should match template id');
67
+ assert(executedQueries.length === 1, 'Should execute 1 query');
68
+ assert(executedQueries[0].sql.includes('$1'), 'PostgreSQL should use $1 placeholder');
69
+
70
+ // Test upsert Postgres query
71
+ await repo.upsertMapping(100, 'updated.event', false);
72
+ assert(executedQueries.length === 2, 'Should execute another query');
73
+ assert(executedQueries[1].sql.includes('ON CONFLICT (template_id)'), 'Postgres query should have ON CONFLICT');
74
+ });
75
+
76
+ // 2. REPOSITORY TESTS (SQL MySQL Dialect)
77
+ runTest('Repository - MySQL Queries', async () => {
78
+ const executedQueries: { sql: string; params: any[] }[] = [];
79
+ const mockMySQLClient: SQLClient = {
80
+ query: async (sql: string, params: any[]) => {
81
+ executedQueries.push({ sql, params });
82
+ // return MySQL row arrays
83
+ return [
84
+ [
85
+ { id: 1, template_id: 200, event_name: 'mysql.event', is_active: true, updated_at: new Date() }
86
+ ],
87
+ {} // Fields meta
88
+ ];
89
+ }
90
+ };
91
+
92
+ const repo = new EmailTemplateRepository({
93
+ brevoApiKey: 'test-key',
94
+ defaultSender: { email: 'test@example.com' },
95
+ dbType: 'mysql',
96
+ dbClient: mockMySQLClient
97
+ });
98
+
99
+ const mapping = await repo.getMappingByTemplateId(200);
100
+ assert(mapping !== null, 'Should return MySQL mapped record');
101
+ assert(mapping?.template_id === 200, 'Should match template id');
102
+ assert(executedQueries.length === 1, 'Should execute 1 query');
103
+ assert(executedQueries[0].sql.includes('?'), 'MySQL should use ? placeholders');
104
+
105
+ // Test upsert MySQL query
106
+ await repo.upsertMapping(200, 'updated.event', true);
107
+ assert(executedQueries.length === 2, 'Should execute upsert query');
108
+ assert(executedQueries[1].sql.includes('ON DUPLICATE KEY UPDATE'), 'MySQL should use ON DUPLICATE KEY UPDATE');
109
+ });
110
+
111
+ // 3. REPOSITORY TESTS (Firestore)
112
+ runTest('Repository - Firestore Collections', async () => {
113
+ const firestoreDbState: Record<string, any> = {};
114
+ let whereClauseCalled = false;
115
+
116
+ const mockFirestoreClient = {
117
+ collection: (collectionName: string) => {
118
+ assert(collectionName === 'email_event_templates', 'Should default collection path');
119
+ return {
120
+ doc: (docId: string) => ({
121
+ get: async () => ({
122
+ exists: firestoreDbState[docId] !== undefined,
123
+ data: () => firestoreDbState[docId],
124
+ }),
125
+ set: async (data: any, options: any) => {
126
+ if (options?.merge && firestoreDbState[docId]) {
127
+ firestoreDbState[docId] = { ...firestoreDbState[docId], ...data };
128
+ } else {
129
+ firestoreDbState[docId] = data;
130
+ }
131
+ }
132
+ }),
133
+ where: (field: string, op: string, val: any) => {
134
+ whereClauseCalled = true;
135
+ return {
136
+ where: () => ({
137
+ limit: () => ({
138
+ get: async () => ({
139
+ empty: false,
140
+ docs: [
141
+ {
142
+ data: () => ({
143
+ template_id: 300,
144
+ event_name: 'firestore.event',
145
+ is_active: true
146
+ })
147
+ }
148
+ ]
149
+ })
150
+ })
151
+ })
152
+ };
153
+ }
154
+ };
155
+ }
156
+ };
157
+
158
+ const repo = new EmailTemplateRepository({
159
+ brevoApiKey: 'test-key',
160
+ defaultSender: { email: 'test@example.com' },
161
+ dbType: 'firestore',
162
+ dbClient: mockFirestoreClient
163
+ });
164
+
165
+ // JIT Sync Mock
166
+ await repo.insertJITMapping(300);
167
+ const cached = await repo.getMappingByTemplateId(300);
168
+ assert(cached !== null, 'Doc should be created JIT');
169
+ assert(cached?.event_name === '', 'JIT mapping event_name should be empty');
170
+
171
+ // Upsert and get
172
+ await repo.upsertMapping(300, 'firestore.event', true);
173
+ const updated = await repo.getMappingByTemplateId(300);
174
+ assert(updated?.event_name === 'firestore.event', 'Updated event should match');
175
+ assert(updated?.is_active === true, 'Updated active state should match');
176
+
177
+ // Active event query
178
+ const active = await repo.getActiveMappingByEvent('firestore.event');
179
+ assert(active !== null, 'Active event record should match query');
180
+ assert(whereClauseCalled === true, 'Should use firestore where filters');
181
+ });
182
+
183
+ // 4. SERVICE VALIDATION TESTS
184
+ runTest('Service - Input Validations', async () => {
185
+ const mockPgClient: SQLClient = { query: async () => ({ rows: [] }) };
186
+ const service = new EmailTemplateService({
187
+ brevoApiKey: 'dummy-key',
188
+ defaultSender: { email: 'test@example.com', name: 'Tester' },
189
+ dbType: 'postgres',
190
+ dbClient: mockPgClient
191
+ });
192
+
193
+ // Test blank name constraint
194
+ try {
195
+ await service.updateTemplate(1, {
196
+ templateName: '',
197
+ subject: 'Sub',
198
+ sender: { email: 'test@example.com' },
199
+ htmlContent: '<html><body>Hello World!</body></html>',
200
+ eventName: 'test',
201
+ isActive: true
202
+ });
203
+ assert(false, 'Should throw error for blank name');
204
+ } catch (e: any) {
205
+ assert(e.message === 'Template name cannot be blank', 'Error message matches blank template validation');
206
+ }
207
+
208
+ // Test minimum HTML content length constraint (<= 10 chars)
209
+ try {
210
+ await service.updateTemplate(1, {
211
+ templateName: 'Valid Name',
212
+ subject: 'Sub',
213
+ sender: { email: 'test@example.com' },
214
+ htmlContent: '<html>',
215
+ eventName: 'test',
216
+ isActive: true
217
+ });
218
+ assert(false, 'Should throw error for short HTML body');
219
+ } catch (e: any) {
220
+ assert(e.message.includes('HTML content must be greater than 10 characters'), 'Error message matches HTML constraint');
221
+ }
222
+ });
223
+
224
+ // Wait a small duration to let asynchronous assertions print out
225
+ setTimeout(() => {
226
+ console.log(`\n๐Ÿ“Š Tests complete. Total Passed: ${testPassedCount}, Failed: ${testFailedCount}`);
227
+ if (testFailedCount > 0) {
228
+ process.exit(1);
229
+ } else {
230
+ console.log('๐ŸŒŸ All unit tests executed successfully without issues!');
231
+ process.exit(0);
232
+ }
233
+ }, 100);
234
+ }
235
+
236
+ runTests().catch((err) => {
237
+ console.error('Fatal crash during test execution:', err);
238
+ process.exit(1);
239
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist", "tests", "svelte", "react"]
16
+ }
@@ -1,4 +1,4 @@
1
- import { Request, Response, Router } from 'express';
1
+ import type { Request, Response, Router } from 'express';
2
2
  import { EmailTemplateService } from './service.js';
3
3
  export declare class EmailTemplateController {
4
4
  private service;
@@ -7,7 +7,7 @@ export declare class EmailTemplateController {
7
7
  * Generates a fully configured Express Router.
8
8
  * Can be directly mounted into any Express app: `app.use('/api/cms', controller.getRouter())`
9
9
  */
10
- getRouter(): Router;
10
+ getRouter(customRouter?: Router): Router;
11
11
  /**
12
12
  * GET /templates
13
13
  * Lists all templates synchronized from Brevo and mapped locally.
@@ -1,4 +1,18 @@
1
- import { Router } from 'express';
1
+ import { createRequire } from 'node:module';
2
+ let cachedExpressRouter;
3
+ function getExpressRouter() {
4
+ if (cachedExpressRouter)
5
+ return cachedExpressRouter;
6
+ try {
7
+ const require = createRequire(import.meta.url);
8
+ const express = require('express');
9
+ cachedExpressRouter = express.Router;
10
+ return cachedExpressRouter;
11
+ }
12
+ catch {
13
+ throw new Error("The 'express' package is required to use EmailTemplateController. Please install express in your project: npm install express");
14
+ }
15
+ }
2
16
  export class EmailTemplateController {
3
17
  service;
4
18
  constructor(service) {
@@ -8,8 +22,8 @@ export class EmailTemplateController {
8
22
  * Generates a fully configured Express Router.
9
23
  * Can be directly mounted into any Express app: `app.use('/api/cms', controller.getRouter())`
10
24
  */
11
- getRouter() {
12
- const router = Router();
25
+ getRouter(customRouter) {
26
+ const router = customRouter || getExpressRouter()();
13
27
  // Standard express mapping
14
28
  router.get('/templates', this.listTemplates.bind(this));
15
29
  router.get('/templates/:id', this.getTemplate.bind(this));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fufulog/brevomorphic-cms-sdk",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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",
@@ -11,6 +11,11 @@
11
11
  "import": "./dist/index.js",
12
12
  "default": "./dist/index.js"
13
13
  },
14
+ "./express": {
15
+ "types": "./dist/controller.d.ts",
16
+ "import": "./dist/controller.js",
17
+ "default": "./dist/controller.js"
18
+ },
14
19
  "./svelte/BrevoWysiwyg.svelte": {
15
20
  "types": "./svelte/BrevoWysiwyg.svelte.d.ts",
16
21
  "import": "./svelte/BrevoWysiwyg.svelte",
@@ -57,5 +62,13 @@
57
62
  },
58
63
  "publishConfig": {
59
64
  "access": "public"
65
+ },
66
+ "peerDependencies": {
67
+ "express": "^4.19.2 || ^5.0.0"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "express": {
71
+ "optional": true
72
+ }
60
73
  }
61
- }
74
+ }
package/src/controller.ts CHANGED
@@ -1,6 +1,23 @@
1
- import { Request, Response, Router } from 'express';
1
+ import type { Request, Response, Router } from 'express';
2
+ import { createRequire } from 'node:module';
2
3
  import { EmailTemplateService } from './service.js';
3
4
 
5
+ let cachedExpressRouter: any;
6
+
7
+ function getExpressRouter(): any {
8
+ if (cachedExpressRouter) return cachedExpressRouter;
9
+ try {
10
+ const require = createRequire(import.meta.url);
11
+ const express = require('express');
12
+ cachedExpressRouter = express.Router;
13
+ return cachedExpressRouter;
14
+ } catch {
15
+ throw new Error(
16
+ "The 'express' package is required to use EmailTemplateController. Please install express in your project: npm install express"
17
+ );
18
+ }
19
+ }
20
+
4
21
  export class EmailTemplateController {
5
22
  private service: EmailTemplateService;
6
23
 
@@ -12,8 +29,8 @@ export class EmailTemplateController {
12
29
  * Generates a fully configured Express Router.
13
30
  * Can be directly mounted into any Express app: `app.use('/api/cms', controller.getRouter())`
14
31
  */
15
- getRouter(): Router {
16
- const router = Router();
32
+ getRouter(customRouter?: Router): Router {
33
+ const router = customRouter || getExpressRouter()();
17
34
 
18
35
  // Standard express mapping
19
36
  router.get('/templates', this.listTemplates.bind(this));
package/tests/sdk.test.ts CHANGED
@@ -221,6 +221,20 @@ async function runTests() {
221
221
  }
222
222
  });
223
223
 
224
+ // 5. CONTROLLER TESTS
225
+ runTest('Controller - getRouter with Express', async () => {
226
+ const mockPgClient: SQLClient = { query: async () => ({ rows: [] }) };
227
+ const service = new EmailTemplateService({
228
+ brevoApiKey: 'dummy-key',
229
+ defaultSender: { email: 'test@example.com' },
230
+ dbType: 'postgres',
231
+ dbClient: mockPgClient
232
+ });
233
+ const controller = new EmailTemplateController(service);
234
+ const router = controller.getRouter();
235
+ assert(typeof router === 'function' || typeof router === 'object', 'Router should be created');
236
+ });
237
+
224
238
  // Wait a small duration to let asynchronous assertions print out
225
239
  setTimeout(() => {
226
240
  console.log(`\n๐Ÿ“Š Tests complete. Total Passed: ${testPassedCount}, Failed: ${testFailedCount}`);