@fufulog/brevomorphic-cms-sdk 1.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.
- package/INTEGRATION.md +101 -0
- package/README.md +336 -0
- package/dist/brevoClient.d.ts +53 -0
- package/dist/brevoClient.js +141 -0
- package/dist/controller.d.ts +46 -0
- package/dist/controller.js +173 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/repository.d.ts +34 -0
- package/dist/repository.js +224 -0
- package/dist/service.d.ts +41 -0
- package/dist/service.js +185 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +1 -0
- package/env.example +25 -0
- package/package.json +35 -0
- package/prd.md +135 -0
- package/react/BrevoWysiwyg.css +233 -0
- package/react/BrevoWysiwyg.tsx +388 -0
- package/src/brevoClient.ts +171 -0
- package/src/controller.ts +186 -0
- package/src/index.ts +5 -0
- package/src/repository.ts +229 -0
- package/src/service.ts +221 -0
- package/src/types.ts +77 -0
- package/svelte/BrevoWysiwyg.svelte +572 -0
- package/tests/sdk.test.ts +239 -0
- package/tsconfig.json +16 -0
|
@@ -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
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -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
|
+
}
|