@digilogiclabs/platform-core 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/README.md +545 -0
- package/dist/ConsoleEmail-XeUBAnwc.d.mts +2786 -0
- package/dist/ConsoleEmail-XeUBAnwc.d.ts +2786 -0
- package/dist/index-C_2W7Byw.d.mts +379 -0
- package/dist/index-C_2W7Byw.d.ts +379 -0
- package/dist/index.d.mts +2045 -0
- package/dist/index.d.ts +2045 -0
- package/dist/index.js +6690 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +6550 -0
- package/dist/index.mjs.map +1 -0
- package/dist/migrate.js +1101 -0
- package/dist/migrate.js.map +1 -0
- package/dist/migrations/index.d.mts +1 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.js +508 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/index.mjs +468 -0
- package/dist/migrations/index.mjs.map +1 -0
- package/dist/testing.d.mts +97 -0
- package/dist/testing.d.ts +97 -0
- package/dist/testing.js +1789 -0
- package/dist/testing.js.map +1 -0
- package/dist/testing.mjs +1743 -0
- package/dist/testing.mjs.map +1 -0
- package/package.json +152 -0
package/dist/testing.mjs
ADDED
|
@@ -0,0 +1,1743 @@
|
|
|
1
|
+
// src/adapters/memory/MemoryDatabase.ts
|
|
2
|
+
var MemoryDatabase = class {
|
|
3
|
+
tables = /* @__PURE__ */ new Map();
|
|
4
|
+
from(table) {
|
|
5
|
+
if (!this.tables.has(table)) {
|
|
6
|
+
this.tables.set(table, []);
|
|
7
|
+
}
|
|
8
|
+
return new MemoryQueryBuilder(this.tables, table);
|
|
9
|
+
}
|
|
10
|
+
async raw(sql, params) {
|
|
11
|
+
console.warn("MemoryDatabase.raw() is a stub implementation");
|
|
12
|
+
return { data: [] };
|
|
13
|
+
}
|
|
14
|
+
async transaction(fn) {
|
|
15
|
+
return fn(this);
|
|
16
|
+
}
|
|
17
|
+
async healthCheck() {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
async close() {
|
|
21
|
+
this.tables.clear();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Clear all data (for testing)
|
|
25
|
+
*/
|
|
26
|
+
clear() {
|
|
27
|
+
this.tables.clear();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get table data (for testing)
|
|
31
|
+
*/
|
|
32
|
+
getTable(tableName) {
|
|
33
|
+
return this.tables.get(tableName) ?? [];
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var MemoryQueryBuilder = class {
|
|
37
|
+
tables;
|
|
38
|
+
tableName;
|
|
39
|
+
_select = ["*"];
|
|
40
|
+
_where = [];
|
|
41
|
+
_orderBy = null;
|
|
42
|
+
_limit = null;
|
|
43
|
+
_offset = 0;
|
|
44
|
+
_insertData = null;
|
|
45
|
+
_updateData = null;
|
|
46
|
+
_deleteFlag = false;
|
|
47
|
+
constructor(tables, tableName) {
|
|
48
|
+
this.tables = tables;
|
|
49
|
+
this.tableName = tableName;
|
|
50
|
+
}
|
|
51
|
+
select(columns) {
|
|
52
|
+
if (columns) {
|
|
53
|
+
this._select = Array.isArray(columns) ? columns : [columns];
|
|
54
|
+
}
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
insert(data) {
|
|
58
|
+
this._insertData = Array.isArray(data) ? data : [data];
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
update(data) {
|
|
62
|
+
this._updateData = data;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
delete() {
|
|
66
|
+
this._deleteFlag = true;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
where(column, operator, value) {
|
|
70
|
+
this._where.push({ column, operator, value });
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
whereIn(column, values) {
|
|
74
|
+
this._where.push({ column, operator: "in", value: values });
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
orderBy(column, direction = "asc") {
|
|
78
|
+
this._orderBy = { column, direction };
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
limit(count) {
|
|
82
|
+
this._limit = count;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
offset(count) {
|
|
86
|
+
this._offset = count;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
async single() {
|
|
90
|
+
const result = await this.execute();
|
|
91
|
+
return { data: result.data[0] || null };
|
|
92
|
+
}
|
|
93
|
+
async execute() {
|
|
94
|
+
const table = this.tables.get(this.tableName) || [];
|
|
95
|
+
if (this._insertData) {
|
|
96
|
+
const newItems = this._insertData.map((item, i) => ({
|
|
97
|
+
id: `mem_${Date.now()}_${i}`,
|
|
98
|
+
...item,
|
|
99
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
100
|
+
}));
|
|
101
|
+
this.tables.set(this.tableName, [...table, ...newItems]);
|
|
102
|
+
return { data: newItems };
|
|
103
|
+
}
|
|
104
|
+
if (this._deleteFlag) {
|
|
105
|
+
const filtered = table.filter((item) => !this.matchesWhere(item));
|
|
106
|
+
const deleted = table.filter((item) => this.matchesWhere(item));
|
|
107
|
+
this.tables.set(this.tableName, filtered);
|
|
108
|
+
return { data: deleted, count: deleted.length };
|
|
109
|
+
}
|
|
110
|
+
if (this._updateData) {
|
|
111
|
+
const updated = [];
|
|
112
|
+
const newTable = table.map((item) => {
|
|
113
|
+
if (this.matchesWhere(item)) {
|
|
114
|
+
const updatedItem = { ...item, ...this._updateData, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
115
|
+
updated.push(updatedItem);
|
|
116
|
+
return updatedItem;
|
|
117
|
+
}
|
|
118
|
+
return item;
|
|
119
|
+
});
|
|
120
|
+
this.tables.set(this.tableName, newTable);
|
|
121
|
+
return { data: updated, count: updated.length };
|
|
122
|
+
}
|
|
123
|
+
let result = table.filter((item) => this.matchesWhere(item));
|
|
124
|
+
if (this._orderBy) {
|
|
125
|
+
const { column, direction } = this._orderBy;
|
|
126
|
+
result.sort((a, b) => {
|
|
127
|
+
const aVal = a[column];
|
|
128
|
+
const bVal = b[column];
|
|
129
|
+
if (aVal === null || aVal === void 0) return direction === "asc" ? 1 : -1;
|
|
130
|
+
if (bVal === null || bVal === void 0) return direction === "asc" ? -1 : 1;
|
|
131
|
+
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
132
|
+
return direction === "asc" ? cmp : -cmp;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (this._offset > 0 || this._limit !== null) {
|
|
136
|
+
const start = this._offset;
|
|
137
|
+
const end = this._limit !== null ? start + this._limit : void 0;
|
|
138
|
+
result = result.slice(start, end);
|
|
139
|
+
}
|
|
140
|
+
return { data: result, count: result.length };
|
|
141
|
+
}
|
|
142
|
+
matchesWhere(item) {
|
|
143
|
+
if (this._where.length === 0) return true;
|
|
144
|
+
return this._where.every(({ column, operator, value }) => {
|
|
145
|
+
const itemValue = item[column];
|
|
146
|
+
switch (operator) {
|
|
147
|
+
case "=":
|
|
148
|
+
case "==":
|
|
149
|
+
return itemValue === value;
|
|
150
|
+
case "!=":
|
|
151
|
+
case "<>":
|
|
152
|
+
return itemValue !== value;
|
|
153
|
+
case ">":
|
|
154
|
+
return itemValue > value;
|
|
155
|
+
case ">=":
|
|
156
|
+
return itemValue >= value;
|
|
157
|
+
case "<":
|
|
158
|
+
return itemValue < value;
|
|
159
|
+
case "<=":
|
|
160
|
+
return itemValue <= value;
|
|
161
|
+
case "in":
|
|
162
|
+
return Array.isArray(value) && value.includes(itemValue);
|
|
163
|
+
case "like":
|
|
164
|
+
return String(itemValue).includes(String(value).replace(/%/g, ""));
|
|
165
|
+
default:
|
|
166
|
+
return itemValue === value;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// src/adapters/memory/MemoryCache.ts
|
|
173
|
+
var MemoryCache = class {
|
|
174
|
+
store = /* @__PURE__ */ new Map();
|
|
175
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
176
|
+
async get(key) {
|
|
177
|
+
const entry = this.store.get(key);
|
|
178
|
+
if (!entry) return null;
|
|
179
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
180
|
+
this.store.delete(key);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return entry.value;
|
|
184
|
+
}
|
|
185
|
+
async set(key, value, ttl) {
|
|
186
|
+
const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
|
|
187
|
+
this.store.set(key, { value, expiresAt });
|
|
188
|
+
}
|
|
189
|
+
async delete(key) {
|
|
190
|
+
this.store.delete(key);
|
|
191
|
+
}
|
|
192
|
+
async exists(key) {
|
|
193
|
+
const value = await this.get(key);
|
|
194
|
+
return value !== null;
|
|
195
|
+
}
|
|
196
|
+
async deletePattern(pattern) {
|
|
197
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
198
|
+
let count = 0;
|
|
199
|
+
for (const key of this.store.keys()) {
|
|
200
|
+
if (regex.test(key)) {
|
|
201
|
+
this.store.delete(key);
|
|
202
|
+
count++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return count;
|
|
206
|
+
}
|
|
207
|
+
async mget(keys) {
|
|
208
|
+
return Promise.all(keys.map((key) => this.get(key)));
|
|
209
|
+
}
|
|
210
|
+
async mset(entries) {
|
|
211
|
+
await Promise.all(entries.map(({ key, value, ttl }) => this.set(key, value, ttl)));
|
|
212
|
+
}
|
|
213
|
+
async incr(key, by = 1) {
|
|
214
|
+
const current = await this.get(key) || 0;
|
|
215
|
+
const newValue = current + by;
|
|
216
|
+
await this.set(key, newValue);
|
|
217
|
+
return newValue;
|
|
218
|
+
}
|
|
219
|
+
async publish(channel, message) {
|
|
220
|
+
const subscribers = this.subscriptions.get(channel);
|
|
221
|
+
if (subscribers) {
|
|
222
|
+
subscribers.forEach((callback) => callback(message));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async subscribe(channel, callback) {
|
|
226
|
+
if (!this.subscriptions.has(channel)) {
|
|
227
|
+
this.subscriptions.set(channel, /* @__PURE__ */ new Set());
|
|
228
|
+
}
|
|
229
|
+
this.subscriptions.get(channel).add(callback);
|
|
230
|
+
return () => {
|
|
231
|
+
this.subscriptions.get(channel)?.delete(callback);
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async healthCheck() {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
async close() {
|
|
238
|
+
this.store.clear();
|
|
239
|
+
this.subscriptions.clear();
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Clear all cached data (for testing)
|
|
243
|
+
*/
|
|
244
|
+
clear() {
|
|
245
|
+
this.store.clear();
|
|
246
|
+
this.subscriptions.clear();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get number of cached items (for testing)
|
|
250
|
+
*/
|
|
251
|
+
get size() {
|
|
252
|
+
return this.store.size;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/adapters/memory/MemoryStorage.ts
|
|
257
|
+
var MemoryStorage = class {
|
|
258
|
+
files = /* @__PURE__ */ new Map();
|
|
259
|
+
async upload(key, data, options) {
|
|
260
|
+
this.files.set(key, { data: Buffer.from("mock"), contentType: options?.contentType });
|
|
261
|
+
return { url: "memory://" + key };
|
|
262
|
+
}
|
|
263
|
+
async download(key) {
|
|
264
|
+
return this.files.get(key)?.data || Buffer.from("");
|
|
265
|
+
}
|
|
266
|
+
async getSignedUrl(key) {
|
|
267
|
+
return "memory://" + key;
|
|
268
|
+
}
|
|
269
|
+
async delete(key) {
|
|
270
|
+
this.files.delete(key);
|
|
271
|
+
}
|
|
272
|
+
async deleteMany(keys) {
|
|
273
|
+
keys.forEach((k) => this.files.delete(k));
|
|
274
|
+
}
|
|
275
|
+
async list(prefix) {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
async exists(key) {
|
|
279
|
+
return this.files.has(key);
|
|
280
|
+
}
|
|
281
|
+
async getMetadata(key) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
async healthCheck() {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Clear all files (for testing)
|
|
289
|
+
*/
|
|
290
|
+
clear() {
|
|
291
|
+
this.files.clear();
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get number of stored files (for testing)
|
|
295
|
+
*/
|
|
296
|
+
get size() {
|
|
297
|
+
return this.files.size;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/adapters/memory/MemoryEmail.ts
|
|
302
|
+
var MemoryEmail = class {
|
|
303
|
+
sentEmails = [];
|
|
304
|
+
async send(message) {
|
|
305
|
+
this.sentEmails.push(message);
|
|
306
|
+
return { id: "mem_" + this.sentEmails.length, success: true };
|
|
307
|
+
}
|
|
308
|
+
async sendBatch(messages) {
|
|
309
|
+
return Promise.all(messages.map((m) => this.send(m)));
|
|
310
|
+
}
|
|
311
|
+
async healthCheck() {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
// Test helpers
|
|
315
|
+
getSentEmails() {
|
|
316
|
+
return this.sentEmails;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Clear sent emails (for testing)
|
|
320
|
+
*/
|
|
321
|
+
clear() {
|
|
322
|
+
this.sentEmails = [];
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Get number of sent emails (for testing)
|
|
326
|
+
*/
|
|
327
|
+
get size() {
|
|
328
|
+
return this.sentEmails.length;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// src/adapters/memory/MemoryQueue.ts
|
|
333
|
+
var MemoryQueue = class {
|
|
334
|
+
jobs = [];
|
|
335
|
+
handlers = [];
|
|
336
|
+
async add(name, data, options) {
|
|
337
|
+
const job = { id: "job_" + this.jobs.length, name, data, attemptsMade: 0, progress: 0, timestamp: Date.now() };
|
|
338
|
+
this.jobs.push(job);
|
|
339
|
+
this.handlers.forEach((h) => h(job));
|
|
340
|
+
return job;
|
|
341
|
+
}
|
|
342
|
+
async addBulk(jobs) {
|
|
343
|
+
return Promise.all(jobs.map((j) => this.add(j.name, j.data, j.options)));
|
|
344
|
+
}
|
|
345
|
+
process(handler) {
|
|
346
|
+
this.handlers.push(handler);
|
|
347
|
+
}
|
|
348
|
+
async getJob(id) {
|
|
349
|
+
return this.jobs.find((j) => j.id === id) || null;
|
|
350
|
+
}
|
|
351
|
+
async removeJob(id) {
|
|
352
|
+
this.jobs = this.jobs.filter((j) => j.id !== id);
|
|
353
|
+
}
|
|
354
|
+
async pause() {
|
|
355
|
+
}
|
|
356
|
+
async resume() {
|
|
357
|
+
}
|
|
358
|
+
async getStats() {
|
|
359
|
+
return { waiting: 0, active: 0, completed: this.jobs.length, failed: 0, delayed: 0 };
|
|
360
|
+
}
|
|
361
|
+
async healthCheck() {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
async close() {
|
|
365
|
+
this.jobs = [];
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Clear all jobs (for testing)
|
|
369
|
+
*/
|
|
370
|
+
clear() {
|
|
371
|
+
this.jobs = [];
|
|
372
|
+
this.handlers = [];
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Get all jobs (for testing)
|
|
376
|
+
*/
|
|
377
|
+
getJobs() {
|
|
378
|
+
return this.jobs;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Get pending jobs (for testing)
|
|
382
|
+
*/
|
|
383
|
+
getPendingJobs() {
|
|
384
|
+
return this.jobs.filter((j) => j.progress < 100);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Get number of jobs (for testing)
|
|
388
|
+
*/
|
|
389
|
+
get size() {
|
|
390
|
+
return this.jobs.length;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/adapters/console/ConsoleEmail.ts
|
|
395
|
+
var ConsoleEmail = class {
|
|
396
|
+
sentEmails = [];
|
|
397
|
+
async send(message) {
|
|
398
|
+
const id = `console_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
399
|
+
console.log("\n" + "=".repeat(60));
|
|
400
|
+
console.log("\u{1F4E7} EMAIL SENT (Console Adapter)");
|
|
401
|
+
console.log("=".repeat(60));
|
|
402
|
+
console.log(`ID: ${id}`);
|
|
403
|
+
console.log(`To: ${this.formatAddresses(message.to)}`);
|
|
404
|
+
console.log(`From: ${message.from ? this.formatAddress(message.from) : "(default)"}`);
|
|
405
|
+
console.log(`Subject: ${message.subject}`);
|
|
406
|
+
if (message.replyTo) {
|
|
407
|
+
console.log(`Reply-To: ${this.formatAddress(message.replyTo)}`);
|
|
408
|
+
}
|
|
409
|
+
if (message.tags && message.tags.length > 0) {
|
|
410
|
+
console.log(`Tags: ${message.tags.join(", ")}`);
|
|
411
|
+
}
|
|
412
|
+
if (message.attachments && message.attachments.length > 0) {
|
|
413
|
+
console.log(`Attachments: ${message.attachments.map((a) => a.filename).join(", ")}`);
|
|
414
|
+
}
|
|
415
|
+
console.log("-".repeat(60));
|
|
416
|
+
if (message.text) {
|
|
417
|
+
console.log("TEXT BODY:");
|
|
418
|
+
console.log(message.text.slice(0, 500) + (message.text.length > 500 ? "\n...(truncated)" : ""));
|
|
419
|
+
}
|
|
420
|
+
if (message.html) {
|
|
421
|
+
console.log("HTML BODY: [HTML content - " + message.html.length + " chars]");
|
|
422
|
+
}
|
|
423
|
+
console.log("=".repeat(60) + "\n");
|
|
424
|
+
this.sentEmails.push(message);
|
|
425
|
+
return {
|
|
426
|
+
id,
|
|
427
|
+
success: true
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async sendBatch(messages) {
|
|
431
|
+
const results = [];
|
|
432
|
+
for (const message of messages) {
|
|
433
|
+
results.push(await this.send(message));
|
|
434
|
+
}
|
|
435
|
+
return results;
|
|
436
|
+
}
|
|
437
|
+
async healthCheck() {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Get all sent emails (for testing)
|
|
442
|
+
*/
|
|
443
|
+
getSentEmails() {
|
|
444
|
+
return [...this.sentEmails];
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Clear sent emails (for testing)
|
|
448
|
+
*/
|
|
449
|
+
clearSentEmails() {
|
|
450
|
+
this.sentEmails = [];
|
|
451
|
+
}
|
|
452
|
+
formatAddress(address) {
|
|
453
|
+
if (address.name) {
|
|
454
|
+
return `${address.name} <${address.email}>`;
|
|
455
|
+
}
|
|
456
|
+
return address.email;
|
|
457
|
+
}
|
|
458
|
+
formatAddresses(addresses) {
|
|
459
|
+
const list = Array.isArray(addresses) ? addresses : [addresses];
|
|
460
|
+
return list.map((addr) => this.formatAddress(addr)).join(", ");
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/interfaces/ISecrets.ts
|
|
465
|
+
var EnvSecrets = class {
|
|
466
|
+
prefix;
|
|
467
|
+
cache = /* @__PURE__ */ new Map();
|
|
468
|
+
constructor(options = {}) {
|
|
469
|
+
this.prefix = options.prefix ?? "";
|
|
470
|
+
}
|
|
471
|
+
getEnvKey(key) {
|
|
472
|
+
const normalizedKey = key.toUpperCase().replace(/[.-]/g, "_");
|
|
473
|
+
return this.prefix ? `${this.prefix}_${normalizedKey}` : normalizedKey;
|
|
474
|
+
}
|
|
475
|
+
async get(key, options) {
|
|
476
|
+
if (options?.cacheTtl) {
|
|
477
|
+
const cached = this.cache.get(key);
|
|
478
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
479
|
+
return cached.value;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const envKey = this.getEnvKey(key);
|
|
483
|
+
const value = process.env[envKey] ?? null;
|
|
484
|
+
if (value && options?.cacheTtl) {
|
|
485
|
+
this.cache.set(key, {
|
|
486
|
+
value,
|
|
487
|
+
expiresAt: Date.now() + options.cacheTtl * 1e3
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
async getWithMetadata(key, options) {
|
|
493
|
+
const value = await this.get(key, options);
|
|
494
|
+
if (value === null) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
key,
|
|
499
|
+
value,
|
|
500
|
+
metadata: {
|
|
501
|
+
// Environment variables don't have native metadata
|
|
502
|
+
createdAt: void 0,
|
|
503
|
+
updatedAt: void 0
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async mget(keys) {
|
|
508
|
+
const result = /* @__PURE__ */ new Map();
|
|
509
|
+
for (const key of keys) {
|
|
510
|
+
result.set(key, await this.get(key));
|
|
511
|
+
}
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
async set(key, value, _options) {
|
|
515
|
+
const envKey = this.getEnvKey(key);
|
|
516
|
+
process.env[envKey] = value;
|
|
517
|
+
this.cache.delete(key);
|
|
518
|
+
}
|
|
519
|
+
async delete(key) {
|
|
520
|
+
const envKey = this.getEnvKey(key);
|
|
521
|
+
delete process.env[envKey];
|
|
522
|
+
this.cache.delete(key);
|
|
523
|
+
}
|
|
524
|
+
async exists(key) {
|
|
525
|
+
const envKey = this.getEnvKey(key);
|
|
526
|
+
return process.env[envKey] !== void 0;
|
|
527
|
+
}
|
|
528
|
+
async list(prefix) {
|
|
529
|
+
const keys = [];
|
|
530
|
+
const envPrefix = this.prefix ? `${this.prefix}_` : "";
|
|
531
|
+
const searchPrefix = prefix ? `${envPrefix}${prefix.toUpperCase().replace(/[.-]/g, "_")}` : envPrefix;
|
|
532
|
+
for (const key of Object.keys(process.env)) {
|
|
533
|
+
if (key.startsWith(searchPrefix || "")) {
|
|
534
|
+
const normalizedKey = key.slice(envPrefix.length).toLowerCase().replace(/_/g, "-");
|
|
535
|
+
keys.push(normalizedKey);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return keys;
|
|
539
|
+
}
|
|
540
|
+
async rotate(key, options) {
|
|
541
|
+
const previousValue = await this.get(key);
|
|
542
|
+
const newValue = options?.rotationFn ? await options.rotationFn() : this.generateSecureValue();
|
|
543
|
+
await this.set(key, newValue);
|
|
544
|
+
return {
|
|
545
|
+
newValue,
|
|
546
|
+
previousValue: previousValue ?? void 0,
|
|
547
|
+
newVersion: "current",
|
|
548
|
+
previousVersion: previousValue ? "previous" : void 0
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async getVersions(_key) {
|
|
552
|
+
return [];
|
|
553
|
+
}
|
|
554
|
+
async healthCheck() {
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
generateSecureValue(length = 32) {
|
|
558
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
|
|
559
|
+
let result = "";
|
|
560
|
+
const randomValues = new Uint32Array(length);
|
|
561
|
+
crypto.getRandomValues(randomValues);
|
|
562
|
+
for (let i = 0; i < length; i++) {
|
|
563
|
+
result += chars[randomValues[i] % chars.length];
|
|
564
|
+
}
|
|
565
|
+
return result;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Clear the internal cache
|
|
569
|
+
*/
|
|
570
|
+
clearCache() {
|
|
571
|
+
this.cache.clear();
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
var MemorySecrets = class {
|
|
575
|
+
secrets = /* @__PURE__ */ new Map();
|
|
576
|
+
versions = /* @__PURE__ */ new Map();
|
|
577
|
+
async get(key, _options) {
|
|
578
|
+
const secret = this.secrets.get(key);
|
|
579
|
+
if (!secret) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
if (secret.metadata?.expiresAt && secret.metadata.expiresAt < /* @__PURE__ */ new Date()) {
|
|
583
|
+
await this.delete(key);
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
return secret.value;
|
|
587
|
+
}
|
|
588
|
+
async getWithMetadata(key, options) {
|
|
589
|
+
const value = await this.get(key, options);
|
|
590
|
+
if (value === null) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
return this.secrets.get(key) ?? null;
|
|
594
|
+
}
|
|
595
|
+
async mget(keys) {
|
|
596
|
+
const result = /* @__PURE__ */ new Map();
|
|
597
|
+
for (const key of keys) {
|
|
598
|
+
result.set(key, await this.get(key));
|
|
599
|
+
}
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
async set(key, value, options) {
|
|
603
|
+
const now = /* @__PURE__ */ new Date();
|
|
604
|
+
const version = `v${Date.now()}`;
|
|
605
|
+
const currentSecret = this.secrets.get(key);
|
|
606
|
+
if (currentSecret?.metadata) {
|
|
607
|
+
const history = this.versions.get(key) ?? [];
|
|
608
|
+
history.push(currentSecret.metadata);
|
|
609
|
+
this.versions.set(key, history);
|
|
610
|
+
}
|
|
611
|
+
this.secrets.set(key, {
|
|
612
|
+
key,
|
|
613
|
+
value,
|
|
614
|
+
metadata: {
|
|
615
|
+
version,
|
|
616
|
+
createdAt: currentSecret?.metadata?.createdAt ?? now,
|
|
617
|
+
updatedAt: now,
|
|
618
|
+
expiresAt: options?.expiresAt,
|
|
619
|
+
tags: options?.tags
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
async delete(key) {
|
|
624
|
+
this.secrets.delete(key);
|
|
625
|
+
this.versions.delete(key);
|
|
626
|
+
}
|
|
627
|
+
async exists(key) {
|
|
628
|
+
return this.secrets.has(key);
|
|
629
|
+
}
|
|
630
|
+
async list(prefix) {
|
|
631
|
+
const keys = [];
|
|
632
|
+
for (const key of this.secrets.keys()) {
|
|
633
|
+
if (!prefix || key.startsWith(prefix)) {
|
|
634
|
+
keys.push(key);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return keys;
|
|
638
|
+
}
|
|
639
|
+
async rotate(key, options) {
|
|
640
|
+
const currentSecret = this.secrets.get(key);
|
|
641
|
+
const previousValue = currentSecret?.value;
|
|
642
|
+
const previousVersion = currentSecret?.metadata?.version;
|
|
643
|
+
const newValue = options?.rotationFn ? await options.rotationFn() : this.generateSecureValue();
|
|
644
|
+
await this.set(key, newValue);
|
|
645
|
+
const newVersion = this.secrets.get(key)?.metadata?.version ?? "unknown";
|
|
646
|
+
return {
|
|
647
|
+
newValue,
|
|
648
|
+
previousValue,
|
|
649
|
+
newVersion,
|
|
650
|
+
previousVersion
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
async getVersions(key) {
|
|
654
|
+
const current = this.secrets.get(key)?.metadata;
|
|
655
|
+
const history = this.versions.get(key) ?? [];
|
|
656
|
+
if (current) {
|
|
657
|
+
return [...history, current];
|
|
658
|
+
}
|
|
659
|
+
return history;
|
|
660
|
+
}
|
|
661
|
+
async healthCheck() {
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
generateSecureValue(length = 32) {
|
|
665
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
|
|
666
|
+
let result = "";
|
|
667
|
+
for (let i = 0; i < length; i++) {
|
|
668
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
669
|
+
}
|
|
670
|
+
return result;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Clear all secrets (for testing)
|
|
674
|
+
*/
|
|
675
|
+
clear() {
|
|
676
|
+
this.secrets.clear();
|
|
677
|
+
this.versions.clear();
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Get count of secrets (for testing)
|
|
681
|
+
*/
|
|
682
|
+
get size() {
|
|
683
|
+
return this.secrets.size;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// src/config.ts
|
|
688
|
+
import { z } from "zod";
|
|
689
|
+
var DatabaseProviderSchema = z.enum(["memory", "postgres", "supabase"]);
|
|
690
|
+
var CacheProviderSchema = z.enum(["memory", "redis", "upstash"]);
|
|
691
|
+
var StorageProviderSchema = z.enum(["memory", "s3", "minio", "r2", "supabase"]);
|
|
692
|
+
var EmailProviderSchema = z.enum(["memory", "console", "smtp", "resend"]);
|
|
693
|
+
var QueueProviderSchema = z.enum(["memory", "bullmq"]);
|
|
694
|
+
var TracingProviderSchema = z.enum(["noop", "memory", "otlp"]);
|
|
695
|
+
var LogLevelSchema = z.enum(["debug", "info", "warn", "error"]);
|
|
696
|
+
var DatabaseConfigSchema = z.object({
|
|
697
|
+
provider: DatabaseProviderSchema.default("memory"),
|
|
698
|
+
url: z.string().optional().describe("PostgreSQL connection URL"),
|
|
699
|
+
connectionString: z.string().optional().describe("PostgreSQL connection string (alias for url)"),
|
|
700
|
+
supabaseUrl: z.string().url().optional().describe("Supabase project URL"),
|
|
701
|
+
supabaseAnonKey: z.string().optional().describe("Supabase anonymous key"),
|
|
702
|
+
supabaseServiceRoleKey: z.string().optional().describe("Supabase service role key"),
|
|
703
|
+
poolSize: z.number().int().min(1).max(100).default(10).describe("Connection pool size"),
|
|
704
|
+
connectionTimeout: z.number().int().min(1e3).max(6e4).default(5e3).describe("Connection timeout in ms"),
|
|
705
|
+
ssl: z.union([z.boolean(), z.object({ rejectUnauthorized: z.boolean().optional() })]).optional().describe("SSL configuration")
|
|
706
|
+
}).refine(
|
|
707
|
+
(data) => {
|
|
708
|
+
if (data.provider === "supabase") {
|
|
709
|
+
return data.supabaseUrl && data.supabaseAnonKey;
|
|
710
|
+
}
|
|
711
|
+
if (data.provider === "postgres") {
|
|
712
|
+
return data.url || data.connectionString;
|
|
713
|
+
}
|
|
714
|
+
return true;
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
message: "Supabase requires supabaseUrl and supabaseAnonKey; PostgreSQL requires url or connectionString"
|
|
718
|
+
}
|
|
719
|
+
);
|
|
720
|
+
var CacheConfigSchema = z.object({
|
|
721
|
+
provider: CacheProviderSchema.default("memory"),
|
|
722
|
+
url: z.string().url().optional().describe("Redis connection URL"),
|
|
723
|
+
upstashUrl: z.string().url().optional().describe("Upstash REST URL"),
|
|
724
|
+
upstashToken: z.string().optional().describe("Upstash REST token"),
|
|
725
|
+
defaultTTL: z.number().int().min(0).default(3600).describe("Default TTL in seconds"),
|
|
726
|
+
keyPrefix: z.string().default("").describe("Prefix for all cache keys"),
|
|
727
|
+
maxRetries: z.number().int().min(0).max(10).default(3).describe("Max retries for cache operations")
|
|
728
|
+
}).refine(
|
|
729
|
+
(data) => {
|
|
730
|
+
if (data.provider === "upstash") {
|
|
731
|
+
return data.upstashUrl && data.upstashToken;
|
|
732
|
+
}
|
|
733
|
+
if (data.provider === "redis") {
|
|
734
|
+
return data.url;
|
|
735
|
+
}
|
|
736
|
+
return true;
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
message: "Upstash requires upstashUrl and upstashToken; Redis requires url"
|
|
740
|
+
}
|
|
741
|
+
);
|
|
742
|
+
var StorageConfigSchema = z.object({
|
|
743
|
+
provider: StorageProviderSchema.default("memory"),
|
|
744
|
+
endpoint: z.string().url().optional().describe("S3-compatible endpoint URL"),
|
|
745
|
+
region: z.string().default("us-east-1").describe("AWS region"),
|
|
746
|
+
accessKey: z.string().optional().describe("AWS access key ID"),
|
|
747
|
+
secretKey: z.string().optional().describe("AWS secret access key"),
|
|
748
|
+
bucket: z.string().optional().describe("S3 bucket name"),
|
|
749
|
+
publicUrl: z.string().url().optional().describe("Public URL for accessing files"),
|
|
750
|
+
forcePathStyle: z.boolean().default(false).describe("Use path-style URLs (required for MinIO)"),
|
|
751
|
+
signedUrlExpiry: z.number().int().min(60).max(604800).default(3600).describe("Signed URL expiry in seconds")
|
|
752
|
+
}).refine(
|
|
753
|
+
(data) => {
|
|
754
|
+
if (["s3", "minio", "r2"].includes(data.provider)) {
|
|
755
|
+
return data.accessKey && data.secretKey && data.bucket;
|
|
756
|
+
}
|
|
757
|
+
return true;
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
message: "S3/MinIO/R2 requires accessKey, secretKey, and bucket"
|
|
761
|
+
}
|
|
762
|
+
);
|
|
763
|
+
var EmailConfigSchema = z.object({
|
|
764
|
+
provider: EmailProviderSchema.default("memory"),
|
|
765
|
+
host: z.string().optional().describe("SMTP host"),
|
|
766
|
+
port: z.number().int().min(1).max(65535).optional().describe("SMTP port"),
|
|
767
|
+
secure: z.boolean().default(false).describe("Use TLS for SMTP"),
|
|
768
|
+
username: z.string().optional().describe("SMTP username"),
|
|
769
|
+
password: z.string().optional().describe("SMTP password"),
|
|
770
|
+
apiKey: z.string().optional().describe("Resend API key"),
|
|
771
|
+
from: z.string().email().or(z.string().regex(/^.+\s<.+@.+>$/)).optional().describe("Default from address"),
|
|
772
|
+
replyTo: z.string().email().optional().describe("Default reply-to address"),
|
|
773
|
+
rateLimitPerSecond: z.number().int().min(1).max(100).default(10).describe("Max emails per second")
|
|
774
|
+
}).refine(
|
|
775
|
+
(data) => {
|
|
776
|
+
if (data.provider === "smtp") {
|
|
777
|
+
return data.host && data.port;
|
|
778
|
+
}
|
|
779
|
+
if (data.provider === "resend") {
|
|
780
|
+
return data.apiKey;
|
|
781
|
+
}
|
|
782
|
+
return true;
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
message: "SMTP requires host and port; Resend requires apiKey"
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
var QueueConfigSchema = z.object({
|
|
789
|
+
provider: QueueProviderSchema.default("memory"),
|
|
790
|
+
redisUrl: z.string().url().optional().describe("Redis connection URL for BullMQ"),
|
|
791
|
+
queueName: z.string().default("platform-jobs").describe("Default queue name"),
|
|
792
|
+
concurrency: z.number().int().min(1).max(100).default(5).describe("Worker concurrency"),
|
|
793
|
+
maxRetries: z.number().int().min(0).max(10).default(3).describe("Max job retries"),
|
|
794
|
+
retryDelay: z.number().int().min(0).default(1e3).describe("Retry delay in ms"),
|
|
795
|
+
removeOnComplete: z.boolean().default(true).describe("Remove completed jobs"),
|
|
796
|
+
removeOnFail: z.boolean().default(false).describe("Remove failed jobs")
|
|
797
|
+
}).refine(
|
|
798
|
+
(data) => {
|
|
799
|
+
if (data.provider === "bullmq") {
|
|
800
|
+
return data.redisUrl;
|
|
801
|
+
}
|
|
802
|
+
return true;
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
message: "BullMQ requires redisUrl"
|
|
806
|
+
}
|
|
807
|
+
);
|
|
808
|
+
var RetryConfigSchema = z.object({
|
|
809
|
+
enabled: z.boolean().default(true).describe("Enable retry for failed operations"),
|
|
810
|
+
maxAttempts: z.number().int().min(1).max(10).default(3).describe("Maximum retry attempts"),
|
|
811
|
+
baseDelay: z.number().int().min(0).max(1e4).default(100).describe("Base delay in ms"),
|
|
812
|
+
maxDelay: z.number().int().min(0).max(6e4).default(5e3).describe("Maximum delay in ms"),
|
|
813
|
+
backoffMultiplier: z.number().min(1).max(5).default(2).describe("Exponential backoff multiplier"),
|
|
814
|
+
jitter: z.boolean().default(true).describe("Add jitter to retry delays")
|
|
815
|
+
});
|
|
816
|
+
var CircuitBreakerConfigSchema = z.object({
|
|
817
|
+
enabled: z.boolean().default(true).describe("Enable circuit breaker"),
|
|
818
|
+
failureThreshold: z.number().int().min(1).max(100).default(5).describe("Failures before opening circuit"),
|
|
819
|
+
resetTimeout: z.number().int().min(1e3).max(3e5).default(3e4).describe("Reset timeout in ms"),
|
|
820
|
+
halfOpenRequests: z.number().int().min(1).max(10).default(3).describe("Requests allowed in half-open state"),
|
|
821
|
+
monitorInterval: z.number().int().min(1e3).max(6e4).default(1e4).describe("Health monitor interval in ms")
|
|
822
|
+
});
|
|
823
|
+
var TimeoutConfigSchema = z.object({
|
|
824
|
+
enabled: z.boolean().default(true).describe("Enable operation timeouts"),
|
|
825
|
+
default: z.number().int().min(100).max(3e5).default(3e4).describe("Default timeout in ms"),
|
|
826
|
+
database: z.number().int().min(100).max(3e5).default(1e4).describe("Database operation timeout"),
|
|
827
|
+
cache: z.number().int().min(100).max(6e4).default(5e3).describe("Cache operation timeout"),
|
|
828
|
+
storage: z.number().int().min(100).max(6e5).default(6e4).describe("Storage operation timeout"),
|
|
829
|
+
email: z.number().int().min(100).max(12e4).default(3e4).describe("Email operation timeout"),
|
|
830
|
+
queue: z.number().int().min(100).max(6e4).default(1e4).describe("Queue operation timeout")
|
|
831
|
+
});
|
|
832
|
+
var BulkheadConfigSchema = z.object({
|
|
833
|
+
enabled: z.boolean().default(false).describe("Enable bulkhead isolation"),
|
|
834
|
+
maxConcurrent: z.number().int().min(1).max(1e3).default(10).describe("Maximum concurrent operations"),
|
|
835
|
+
maxQueued: z.number().int().min(0).max(1e4).default(100).describe("Maximum queued operations"),
|
|
836
|
+
timeout: z.number().int().min(0).max(3e5).default(3e4).describe("Queue timeout in ms")
|
|
837
|
+
});
|
|
838
|
+
var ResilienceConfigSchema = z.object({
|
|
839
|
+
retry: RetryConfigSchema.default({}),
|
|
840
|
+
circuitBreaker: CircuitBreakerConfigSchema.default({}),
|
|
841
|
+
timeout: TimeoutConfigSchema.default({}),
|
|
842
|
+
bulkhead: BulkheadConfigSchema.default({})
|
|
843
|
+
});
|
|
844
|
+
var LoggingConfigSchema = z.object({
|
|
845
|
+
level: LogLevelSchema.default("info").describe("Minimum log level"),
|
|
846
|
+
format: z.enum(["json", "pretty"]).default("json").describe("Log output format"),
|
|
847
|
+
includeTimestamp: z.boolean().default(true).describe("Include timestamp in logs"),
|
|
848
|
+
includeCorrelationId: z.boolean().default(true).describe("Include correlation ID in logs"),
|
|
849
|
+
redactKeys: z.array(z.string()).default(["password", "token", "secret", "apiKey", "authorization"]).describe("Keys to redact from logs")
|
|
850
|
+
});
|
|
851
|
+
var MetricsConfigSchema = z.object({
|
|
852
|
+
enabled: z.boolean().default(false).describe("Enable metrics collection"),
|
|
853
|
+
prefix: z.string().default("platform").describe("Metric name prefix"),
|
|
854
|
+
defaultTags: z.record(z.string()).default({}).describe("Default tags for all metrics"),
|
|
855
|
+
flushInterval: z.number().int().min(1e3).max(6e4).default(1e4).describe("Flush interval in ms"),
|
|
856
|
+
histogramBuckets: z.array(z.number()).default([5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3, 1e4]).describe("Histogram bucket boundaries in ms")
|
|
857
|
+
});
|
|
858
|
+
var TracingConfigSchema = z.object({
|
|
859
|
+
enabled: z.boolean().default(false).describe("Enable distributed tracing"),
|
|
860
|
+
provider: TracingProviderSchema.default("noop").describe("Tracing provider"),
|
|
861
|
+
serviceName: z.string().optional().describe("Service name for traces"),
|
|
862
|
+
serviceVersion: z.string().optional().describe("Service version"),
|
|
863
|
+
environment: z.string().optional().describe("Environment (dev, staging, production)"),
|
|
864
|
+
sampleRate: z.number().min(0).max(1).default(1).describe("Trace sampling rate"),
|
|
865
|
+
propagateContext: z.boolean().default(true).describe("Propagate trace context to downstream services"),
|
|
866
|
+
endpoint: z.string().url().optional().describe("OTLP exporter endpoint"),
|
|
867
|
+
exporterType: z.enum(["otlp-http", "otlp-grpc", "console"]).default("otlp-http").describe("Exporter type")
|
|
868
|
+
});
|
|
869
|
+
var ObservabilityConfigSchema = z.object({
|
|
870
|
+
logging: LoggingConfigSchema.default({}),
|
|
871
|
+
metrics: MetricsConfigSchema.default({}),
|
|
872
|
+
tracing: TracingConfigSchema.default({})
|
|
873
|
+
});
|
|
874
|
+
var MiddlewareConfigSchema = z.object({
|
|
875
|
+
enabled: z.boolean().default(true).describe("Enable middleware chain"),
|
|
876
|
+
logging: z.boolean().default(true).describe("Enable logging middleware"),
|
|
877
|
+
metrics: z.boolean().default(false).describe("Enable metrics middleware"),
|
|
878
|
+
slowQuery: z.object({
|
|
879
|
+
enabled: z.boolean().default(true),
|
|
880
|
+
thresholdMs: z.number().int().min(1).default(1e3)
|
|
881
|
+
}).default({}),
|
|
882
|
+
cache: z.object({
|
|
883
|
+
enabled: z.boolean().default(false),
|
|
884
|
+
defaultTTL: z.number().int().min(0).default(300)
|
|
885
|
+
}).default({})
|
|
886
|
+
});
|
|
887
|
+
var PlatformConfigSchema = z.object({
|
|
888
|
+
// Service configurations
|
|
889
|
+
database: DatabaseConfigSchema.default({ provider: "memory" }),
|
|
890
|
+
cache: CacheConfigSchema.default({ provider: "memory" }),
|
|
891
|
+
storage: StorageConfigSchema.default({ provider: "memory" }),
|
|
892
|
+
email: EmailConfigSchema.default({ provider: "memory" }),
|
|
893
|
+
queue: QueueConfigSchema.default({ provider: "memory" }),
|
|
894
|
+
// Resilience configuration
|
|
895
|
+
resilience: ResilienceConfigSchema.default({}),
|
|
896
|
+
// Observability configuration
|
|
897
|
+
observability: ObservabilityConfigSchema.default({}),
|
|
898
|
+
// Middleware configuration
|
|
899
|
+
middleware: MiddlewareConfigSchema.default({})
|
|
900
|
+
});
|
|
901
|
+
function loadConfig() {
|
|
902
|
+
return PlatformConfigSchema.parse({
|
|
903
|
+
database: {
|
|
904
|
+
provider: process.env.PLATFORM_DB_PROVIDER || process.env.DATABASE_PROVIDER || "memory",
|
|
905
|
+
url: process.env.DATABASE_URL,
|
|
906
|
+
supabaseUrl: process.env.SUPABASE_URL,
|
|
907
|
+
supabaseAnonKey: process.env.SUPABASE_ANON_KEY,
|
|
908
|
+
supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
909
|
+
poolSize: process.env.DATABASE_POOL_SIZE ? parseInt(process.env.DATABASE_POOL_SIZE) : void 0,
|
|
910
|
+
connectionTimeout: process.env.DATABASE_TIMEOUT ? parseInt(process.env.DATABASE_TIMEOUT) : void 0
|
|
911
|
+
},
|
|
912
|
+
cache: {
|
|
913
|
+
provider: process.env.PLATFORM_CACHE_PROVIDER || process.env.CACHE_PROVIDER || "memory",
|
|
914
|
+
url: process.env.REDIS_URL,
|
|
915
|
+
upstashUrl: process.env.UPSTASH_REDIS_REST_URL,
|
|
916
|
+
upstashToken: process.env.UPSTASH_REDIS_REST_TOKEN,
|
|
917
|
+
defaultTTL: process.env.CACHE_DEFAULT_TTL ? parseInt(process.env.CACHE_DEFAULT_TTL) : void 0,
|
|
918
|
+
keyPrefix: process.env.CACHE_KEY_PREFIX
|
|
919
|
+
},
|
|
920
|
+
storage: {
|
|
921
|
+
provider: process.env.PLATFORM_STORAGE_PROVIDER || process.env.STORAGE_PROVIDER || "memory",
|
|
922
|
+
endpoint: process.env.S3_ENDPOINT,
|
|
923
|
+
region: process.env.S3_REGION || process.env.AWS_REGION || "us-east-1",
|
|
924
|
+
accessKey: process.env.S3_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID,
|
|
925
|
+
secretKey: process.env.S3_SECRET_ACCESS_KEY || process.env.AWS_SECRET_ACCESS_KEY,
|
|
926
|
+
bucket: process.env.S3_BUCKET,
|
|
927
|
+
publicUrl: process.env.S3_PUBLIC_URL,
|
|
928
|
+
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true",
|
|
929
|
+
signedUrlExpiry: process.env.S3_SIGNED_URL_EXPIRY ? parseInt(process.env.S3_SIGNED_URL_EXPIRY) : void 0
|
|
930
|
+
},
|
|
931
|
+
email: {
|
|
932
|
+
provider: process.env.PLATFORM_EMAIL_PROVIDER || process.env.EMAIL_PROVIDER || "memory",
|
|
933
|
+
host: process.env.SMTP_HOST,
|
|
934
|
+
port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : void 0,
|
|
935
|
+
secure: process.env.SMTP_SECURE === "true",
|
|
936
|
+
username: process.env.SMTP_USERNAME,
|
|
937
|
+
password: process.env.SMTP_PASSWORD,
|
|
938
|
+
apiKey: process.env.RESEND_API_KEY,
|
|
939
|
+
from: process.env.EMAIL_FROM,
|
|
940
|
+
replyTo: process.env.EMAIL_REPLY_TO
|
|
941
|
+
},
|
|
942
|
+
queue: {
|
|
943
|
+
provider: process.env.PLATFORM_QUEUE_PROVIDER || process.env.QUEUE_PROVIDER || "memory",
|
|
944
|
+
redisUrl: process.env.REDIS_URL,
|
|
945
|
+
queueName: process.env.QUEUE_NAME,
|
|
946
|
+
concurrency: process.env.QUEUE_CONCURRENCY ? parseInt(process.env.QUEUE_CONCURRENCY) : void 0,
|
|
947
|
+
maxRetries: process.env.QUEUE_MAX_RETRIES ? parseInt(process.env.QUEUE_MAX_RETRIES) : void 0
|
|
948
|
+
},
|
|
949
|
+
resilience: {
|
|
950
|
+
retry: {
|
|
951
|
+
enabled: process.env.RESILIENCE_RETRY_ENABLED !== "false",
|
|
952
|
+
maxAttempts: process.env.RESILIENCE_RETRY_MAX_ATTEMPTS ? parseInt(process.env.RESILIENCE_RETRY_MAX_ATTEMPTS) : void 0,
|
|
953
|
+
baseDelay: process.env.RESILIENCE_RETRY_BASE_DELAY ? parseInt(process.env.RESILIENCE_RETRY_BASE_DELAY) : void 0,
|
|
954
|
+
maxDelay: process.env.RESILIENCE_RETRY_MAX_DELAY ? parseInt(process.env.RESILIENCE_RETRY_MAX_DELAY) : void 0
|
|
955
|
+
},
|
|
956
|
+
circuitBreaker: {
|
|
957
|
+
enabled: process.env.RESILIENCE_CIRCUIT_BREAKER_ENABLED !== "false",
|
|
958
|
+
failureThreshold: process.env.RESILIENCE_CIRCUIT_BREAKER_THRESHOLD ? parseInt(process.env.RESILIENCE_CIRCUIT_BREAKER_THRESHOLD) : void 0,
|
|
959
|
+
resetTimeout: process.env.RESILIENCE_CIRCUIT_BREAKER_RESET_TIMEOUT ? parseInt(process.env.RESILIENCE_CIRCUIT_BREAKER_RESET_TIMEOUT) : void 0
|
|
960
|
+
},
|
|
961
|
+
timeout: {
|
|
962
|
+
enabled: process.env.RESILIENCE_TIMEOUT_ENABLED !== "false",
|
|
963
|
+
default: process.env.RESILIENCE_TIMEOUT_DEFAULT ? parseInt(process.env.RESILIENCE_TIMEOUT_DEFAULT) : void 0
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
observability: {
|
|
967
|
+
logging: {
|
|
968
|
+
level: process.env.LOG_LEVEL || "info",
|
|
969
|
+
format: process.env.LOG_FORMAT || "json"
|
|
970
|
+
},
|
|
971
|
+
metrics: {
|
|
972
|
+
enabled: process.env.METRICS_ENABLED === "true",
|
|
973
|
+
prefix: process.env.METRICS_PREFIX
|
|
974
|
+
},
|
|
975
|
+
tracing: {
|
|
976
|
+
enabled: process.env.TRACING_ENABLED === "true",
|
|
977
|
+
provider: process.env.TRACING_PROVIDER || "noop",
|
|
978
|
+
serviceName: process.env.SERVICE_NAME || process.env.OTEL_SERVICE_NAME,
|
|
979
|
+
serviceVersion: process.env.SERVICE_VERSION,
|
|
980
|
+
environment: process.env.NODE_ENV,
|
|
981
|
+
sampleRate: process.env.TRACING_SAMPLE_RATE ? parseFloat(process.env.TRACING_SAMPLE_RATE) : void 0,
|
|
982
|
+
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.TRACING_ENDPOINT,
|
|
983
|
+
exporterType: process.env.OTEL_EXPORTER_TYPE || "otlp-http"
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
middleware: {
|
|
987
|
+
enabled: process.env.MIDDLEWARE_ENABLED !== "false",
|
|
988
|
+
logging: process.env.MIDDLEWARE_LOGGING !== "false",
|
|
989
|
+
metrics: process.env.MIDDLEWARE_METRICS === "true",
|
|
990
|
+
slowQuery: {
|
|
991
|
+
enabled: process.env.MIDDLEWARE_SLOW_QUERY !== "false",
|
|
992
|
+
thresholdMs: process.env.MIDDLEWARE_SLOW_QUERY_THRESHOLD ? parseInt(process.env.MIDDLEWARE_SLOW_QUERY_THRESHOLD) : void 0
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/interfaces/ITracing.ts
|
|
999
|
+
var MemorySpan = class {
|
|
1000
|
+
name;
|
|
1001
|
+
context;
|
|
1002
|
+
isRecording = true;
|
|
1003
|
+
_attributes = {};
|
|
1004
|
+
_events = [];
|
|
1005
|
+
_status = { code: "unset" };
|
|
1006
|
+
_endTime;
|
|
1007
|
+
_startTime;
|
|
1008
|
+
constructor(name, traceId, parentSpanId) {
|
|
1009
|
+
this.name = name;
|
|
1010
|
+
this._startTime = Date.now();
|
|
1011
|
+
this.context = {
|
|
1012
|
+
traceId,
|
|
1013
|
+
spanId: this.generateSpanId(),
|
|
1014
|
+
traceFlags: 1
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
generateSpanId() {
|
|
1018
|
+
return Math.random().toString(16).substring(2, 18).padStart(16, "0");
|
|
1019
|
+
}
|
|
1020
|
+
setAttribute(key, value) {
|
|
1021
|
+
this._attributes[key] = value;
|
|
1022
|
+
return this;
|
|
1023
|
+
}
|
|
1024
|
+
setAttributes(attributes) {
|
|
1025
|
+
Object.assign(this._attributes, attributes);
|
|
1026
|
+
return this;
|
|
1027
|
+
}
|
|
1028
|
+
addEvent(name, attributes) {
|
|
1029
|
+
this._events.push({ name, timestamp: Date.now(), attributes });
|
|
1030
|
+
return this;
|
|
1031
|
+
}
|
|
1032
|
+
setStatus(status) {
|
|
1033
|
+
this._status = status;
|
|
1034
|
+
return this;
|
|
1035
|
+
}
|
|
1036
|
+
recordException(exception, attributes) {
|
|
1037
|
+
this.addEvent("exception", {
|
|
1038
|
+
"exception.type": exception.name,
|
|
1039
|
+
"exception.message": exception.message,
|
|
1040
|
+
"exception.stacktrace": exception.stack || "",
|
|
1041
|
+
...attributes
|
|
1042
|
+
});
|
|
1043
|
+
this.setStatus({ code: "error", message: exception.message });
|
|
1044
|
+
return this;
|
|
1045
|
+
}
|
|
1046
|
+
updateName(name) {
|
|
1047
|
+
this.name = name;
|
|
1048
|
+
return this;
|
|
1049
|
+
}
|
|
1050
|
+
end(endTime) {
|
|
1051
|
+
this._endTime = endTime ?? Date.now();
|
|
1052
|
+
}
|
|
1053
|
+
// Testing helpers
|
|
1054
|
+
getAttributes() {
|
|
1055
|
+
return { ...this._attributes };
|
|
1056
|
+
}
|
|
1057
|
+
getEvents() {
|
|
1058
|
+
return [...this._events];
|
|
1059
|
+
}
|
|
1060
|
+
getStatus() {
|
|
1061
|
+
return { ...this._status };
|
|
1062
|
+
}
|
|
1063
|
+
getDuration() {
|
|
1064
|
+
return this._endTime ? this._endTime - this._startTime : void 0;
|
|
1065
|
+
}
|
|
1066
|
+
isEnded() {
|
|
1067
|
+
return this._endTime !== void 0;
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
var MemoryTracing = class {
|
|
1071
|
+
spans = [];
|
|
1072
|
+
currentSpan;
|
|
1073
|
+
traceId;
|
|
1074
|
+
constructor() {
|
|
1075
|
+
this.traceId = this.generateTraceId();
|
|
1076
|
+
}
|
|
1077
|
+
generateTraceId() {
|
|
1078
|
+
return Math.random().toString(16).substring(2, 34).padStart(32, "0");
|
|
1079
|
+
}
|
|
1080
|
+
startSpan(name, options) {
|
|
1081
|
+
const span = new MemorySpan(name, this.traceId, this.currentSpan?.context.spanId);
|
|
1082
|
+
if (options?.attributes) {
|
|
1083
|
+
span.setAttributes(options.attributes);
|
|
1084
|
+
}
|
|
1085
|
+
this.spans.push(span);
|
|
1086
|
+
this.currentSpan = span;
|
|
1087
|
+
return span;
|
|
1088
|
+
}
|
|
1089
|
+
getCurrentSpan() {
|
|
1090
|
+
return this.currentSpan;
|
|
1091
|
+
}
|
|
1092
|
+
withSpan(name, fn, options) {
|
|
1093
|
+
const span = this.startSpan(name, options);
|
|
1094
|
+
try {
|
|
1095
|
+
const result = fn(span);
|
|
1096
|
+
span.setStatus({ code: "ok" });
|
|
1097
|
+
return result;
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
1100
|
+
throw error;
|
|
1101
|
+
} finally {
|
|
1102
|
+
span.end();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
async withSpanAsync(name, fn, options) {
|
|
1106
|
+
const span = this.startSpan(name, options);
|
|
1107
|
+
try {
|
|
1108
|
+
const result = await fn(span);
|
|
1109
|
+
span.setStatus({ code: "ok" });
|
|
1110
|
+
return result;
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
1113
|
+
throw error;
|
|
1114
|
+
} finally {
|
|
1115
|
+
span.end();
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
instrument(name, fn, options) {
|
|
1119
|
+
return (...args) => this.withSpan(name, () => fn(...args), options);
|
|
1120
|
+
}
|
|
1121
|
+
instrumentAsync(name, fn, options) {
|
|
1122
|
+
return (...args) => this.withSpanAsync(name, () => fn(...args), options);
|
|
1123
|
+
}
|
|
1124
|
+
extractContext(headers) {
|
|
1125
|
+
const traceparent = headers["traceparent"];
|
|
1126
|
+
if (!traceparent || typeof traceparent !== "string") return void 0;
|
|
1127
|
+
const parts = traceparent.split("-");
|
|
1128
|
+
if (parts.length !== 4) return void 0;
|
|
1129
|
+
return {
|
|
1130
|
+
traceId: parts[1],
|
|
1131
|
+
spanId: parts[2],
|
|
1132
|
+
traceFlags: parseInt(parts[3], 16)
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
injectContext(headers) {
|
|
1136
|
+
if (this.currentSpan) {
|
|
1137
|
+
const ctx = this.currentSpan.context;
|
|
1138
|
+
headers["traceparent"] = `00-${ctx.traceId}-${ctx.spanId}-${ctx.traceFlags.toString(16).padStart(2, "0")}`;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
async healthCheck() {
|
|
1142
|
+
return true;
|
|
1143
|
+
}
|
|
1144
|
+
async flush() {
|
|
1145
|
+
}
|
|
1146
|
+
async close() {
|
|
1147
|
+
this.spans = [];
|
|
1148
|
+
this.currentSpan = void 0;
|
|
1149
|
+
}
|
|
1150
|
+
// Testing helpers
|
|
1151
|
+
getSpans() {
|
|
1152
|
+
return [...this.spans];
|
|
1153
|
+
}
|
|
1154
|
+
getCompletedSpans() {
|
|
1155
|
+
return this.spans.filter((s) => s.isEnded());
|
|
1156
|
+
}
|
|
1157
|
+
clear() {
|
|
1158
|
+
this.spans = [];
|
|
1159
|
+
this.currentSpan = void 0;
|
|
1160
|
+
this.traceId = this.generateTraceId();
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
var NoopSpan = class {
|
|
1164
|
+
name = "";
|
|
1165
|
+
context = { traceId: "", spanId: "", traceFlags: 0 };
|
|
1166
|
+
isRecording = false;
|
|
1167
|
+
setAttribute() {
|
|
1168
|
+
return this;
|
|
1169
|
+
}
|
|
1170
|
+
setAttributes() {
|
|
1171
|
+
return this;
|
|
1172
|
+
}
|
|
1173
|
+
addEvent() {
|
|
1174
|
+
return this;
|
|
1175
|
+
}
|
|
1176
|
+
setStatus() {
|
|
1177
|
+
return this;
|
|
1178
|
+
}
|
|
1179
|
+
recordException() {
|
|
1180
|
+
return this;
|
|
1181
|
+
}
|
|
1182
|
+
updateName() {
|
|
1183
|
+
return this;
|
|
1184
|
+
}
|
|
1185
|
+
end() {
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
var NoopTracing = class {
|
|
1189
|
+
noopSpan = new NoopSpan();
|
|
1190
|
+
startSpan() {
|
|
1191
|
+
return this.noopSpan;
|
|
1192
|
+
}
|
|
1193
|
+
getCurrentSpan() {
|
|
1194
|
+
return void 0;
|
|
1195
|
+
}
|
|
1196
|
+
withSpan(_name, fn) {
|
|
1197
|
+
return fn(this.noopSpan);
|
|
1198
|
+
}
|
|
1199
|
+
async withSpanAsync(_name, fn) {
|
|
1200
|
+
return fn(this.noopSpan);
|
|
1201
|
+
}
|
|
1202
|
+
instrument(_name, fn) {
|
|
1203
|
+
return fn;
|
|
1204
|
+
}
|
|
1205
|
+
instrumentAsync(_name, fn) {
|
|
1206
|
+
return fn;
|
|
1207
|
+
}
|
|
1208
|
+
extractContext() {
|
|
1209
|
+
return void 0;
|
|
1210
|
+
}
|
|
1211
|
+
injectContext() {
|
|
1212
|
+
}
|
|
1213
|
+
async healthCheck() {
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
async flush() {
|
|
1217
|
+
}
|
|
1218
|
+
async close() {
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
// src/interfaces/ILogger.ts
|
|
1223
|
+
var ConsoleLogger = class _ConsoleLogger {
|
|
1224
|
+
context;
|
|
1225
|
+
level;
|
|
1226
|
+
static levelPriority = {
|
|
1227
|
+
debug: 0,
|
|
1228
|
+
info: 1,
|
|
1229
|
+
warn: 2,
|
|
1230
|
+
error: 3
|
|
1231
|
+
};
|
|
1232
|
+
constructor(config = {}) {
|
|
1233
|
+
this.context = {
|
|
1234
|
+
service: config.service,
|
|
1235
|
+
environment: config.environment
|
|
1236
|
+
};
|
|
1237
|
+
this.level = config.level || "info";
|
|
1238
|
+
}
|
|
1239
|
+
shouldLog(level) {
|
|
1240
|
+
return _ConsoleLogger.levelPriority[level] >= _ConsoleLogger.levelPriority[this.level];
|
|
1241
|
+
}
|
|
1242
|
+
log(level, message, meta) {
|
|
1243
|
+
if (!this.shouldLog(level)) return;
|
|
1244
|
+
const entry = {
|
|
1245
|
+
level,
|
|
1246
|
+
message,
|
|
1247
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1248
|
+
meta,
|
|
1249
|
+
context: this.context
|
|
1250
|
+
};
|
|
1251
|
+
const prefix = `[${entry.timestamp.toISOString()}] [${level.toUpperCase()}]`;
|
|
1252
|
+
const contextStr = Object.keys(this.context).length > 0 ? ` [${Object.entries(this.context).map(([k, v]) => `${k}=${v}`).join(" ")}]` : "";
|
|
1253
|
+
switch (level) {
|
|
1254
|
+
case "debug":
|
|
1255
|
+
console.debug(`${prefix}${contextStr} ${message}`, meta || "");
|
|
1256
|
+
break;
|
|
1257
|
+
case "info":
|
|
1258
|
+
console.info(`${prefix}${contextStr} ${message}`, meta || "");
|
|
1259
|
+
break;
|
|
1260
|
+
case "warn":
|
|
1261
|
+
console.warn(`${prefix}${contextStr} ${message}`, meta || "");
|
|
1262
|
+
break;
|
|
1263
|
+
case "error":
|
|
1264
|
+
console.error(`${prefix}${contextStr} ${message}`, meta || "");
|
|
1265
|
+
if (meta?.error) {
|
|
1266
|
+
console.error(meta.error);
|
|
1267
|
+
}
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
debug(message, meta) {
|
|
1272
|
+
this.log("debug", message, meta);
|
|
1273
|
+
}
|
|
1274
|
+
info(message, meta) {
|
|
1275
|
+
this.log("info", message, meta);
|
|
1276
|
+
}
|
|
1277
|
+
warn(message, meta) {
|
|
1278
|
+
this.log("warn", message, meta);
|
|
1279
|
+
}
|
|
1280
|
+
error(message, meta) {
|
|
1281
|
+
this.log("error", message, meta);
|
|
1282
|
+
}
|
|
1283
|
+
child(context) {
|
|
1284
|
+
const childLogger = new _ConsoleLogger({ level: this.level });
|
|
1285
|
+
childLogger.context = { ...this.context, ...context };
|
|
1286
|
+
return childLogger;
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
var NoopLogger = class {
|
|
1290
|
+
debug() {
|
|
1291
|
+
}
|
|
1292
|
+
info() {
|
|
1293
|
+
}
|
|
1294
|
+
warn() {
|
|
1295
|
+
}
|
|
1296
|
+
error() {
|
|
1297
|
+
}
|
|
1298
|
+
child() {
|
|
1299
|
+
return this;
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
// src/interfaces/IMetrics.ts
|
|
1304
|
+
var MemoryMetrics = class {
|
|
1305
|
+
counters = /* @__PURE__ */ new Map();
|
|
1306
|
+
gauges = /* @__PURE__ */ new Map();
|
|
1307
|
+
histograms = /* @__PURE__ */ new Map();
|
|
1308
|
+
timings = /* @__PURE__ */ new Map();
|
|
1309
|
+
sets = /* @__PURE__ */ new Map();
|
|
1310
|
+
increment(name, value = 1, tags = {}) {
|
|
1311
|
+
const key = this.createKey(name, tags);
|
|
1312
|
+
const existing = this.counters.get(key);
|
|
1313
|
+
if (existing && existing[0]) {
|
|
1314
|
+
existing[0].value += value;
|
|
1315
|
+
} else {
|
|
1316
|
+
this.counters.set(key, [{ value, tags }]);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
decrement(name, value = 1, tags = {}) {
|
|
1320
|
+
this.increment(name, -value, tags);
|
|
1321
|
+
}
|
|
1322
|
+
gauge(name, value, tags = {}) {
|
|
1323
|
+
const key = this.createKey(name, tags);
|
|
1324
|
+
this.gauges.set(key, { value, tags, timestamp: Date.now() });
|
|
1325
|
+
}
|
|
1326
|
+
histogram(name, value, tags = {}) {
|
|
1327
|
+
const key = this.createKey(name, tags);
|
|
1328
|
+
const existing = this.histograms.get(key);
|
|
1329
|
+
if (existing && existing[0]) {
|
|
1330
|
+
existing[0].values.push(value);
|
|
1331
|
+
} else {
|
|
1332
|
+
this.histograms.set(key, [{ values: [value], tags }]);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
timing(name, value, tags = {}) {
|
|
1336
|
+
const key = this.createKey(name, tags);
|
|
1337
|
+
const existing = this.timings.get(key);
|
|
1338
|
+
if (existing && existing[0]) {
|
|
1339
|
+
existing[0].values.push(value);
|
|
1340
|
+
} else {
|
|
1341
|
+
this.timings.set(key, [{ values: [value], tags }]);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
startTimer(name, tags = {}) {
|
|
1345
|
+
const start = Date.now();
|
|
1346
|
+
return () => {
|
|
1347
|
+
const duration = Date.now() - start;
|
|
1348
|
+
this.timing(name, duration, tags);
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
distribution(name, value, tags = {}) {
|
|
1352
|
+
this.histogram(name, value, tags);
|
|
1353
|
+
}
|
|
1354
|
+
set(name, value, tags = {}) {
|
|
1355
|
+
const key = this.createKey(name, tags);
|
|
1356
|
+
let existing = this.sets.get(key);
|
|
1357
|
+
if (!existing) {
|
|
1358
|
+
existing = /* @__PURE__ */ new Set();
|
|
1359
|
+
this.sets.set(key, existing);
|
|
1360
|
+
}
|
|
1361
|
+
existing.add(value);
|
|
1362
|
+
}
|
|
1363
|
+
async flush() {
|
|
1364
|
+
}
|
|
1365
|
+
async close() {
|
|
1366
|
+
}
|
|
1367
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1368
|
+
// Test helpers
|
|
1369
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1370
|
+
/**
|
|
1371
|
+
* Get counter value
|
|
1372
|
+
*/
|
|
1373
|
+
getCounter(name, tags = {}) {
|
|
1374
|
+
const key = this.createKey(name, tags);
|
|
1375
|
+
const data = this.counters.get(key);
|
|
1376
|
+
return data?.[0]?.value ?? 0;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Get gauge value
|
|
1380
|
+
*/
|
|
1381
|
+
getGauge(name, tags = {}) {
|
|
1382
|
+
const key = this.createKey(name, tags);
|
|
1383
|
+
return this.gauges.get(key)?.value;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Get histogram values
|
|
1387
|
+
*/
|
|
1388
|
+
getHistogram(name, tags = {}) {
|
|
1389
|
+
const key = this.createKey(name, tags);
|
|
1390
|
+
return this.histograms.get(key)?.[0]?.values ?? [];
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Get timing values
|
|
1394
|
+
*/
|
|
1395
|
+
getTiming(name, tags = {}) {
|
|
1396
|
+
const key = this.createKey(name, tags);
|
|
1397
|
+
return this.timings.get(key)?.[0]?.values ?? [];
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Get set size
|
|
1401
|
+
*/
|
|
1402
|
+
getSetSize(name, tags = {}) {
|
|
1403
|
+
const key = this.createKey(name, tags);
|
|
1404
|
+
return this.sets.get(key)?.size ?? 0;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Get all metrics as a summary
|
|
1408
|
+
*/
|
|
1409
|
+
getSummary() {
|
|
1410
|
+
const summary = {
|
|
1411
|
+
counters: {},
|
|
1412
|
+
gauges: {},
|
|
1413
|
+
histograms: {},
|
|
1414
|
+
timings: {},
|
|
1415
|
+
sets: {}
|
|
1416
|
+
};
|
|
1417
|
+
for (const [key, data] of this.counters) {
|
|
1418
|
+
const first = data[0];
|
|
1419
|
+
if (first) {
|
|
1420
|
+
summary.counters[key] = first.value;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
for (const [key, data] of this.gauges) {
|
|
1424
|
+
summary.gauges[key] = data.value;
|
|
1425
|
+
}
|
|
1426
|
+
for (const [key, data] of this.histograms) {
|
|
1427
|
+
const first = data[0];
|
|
1428
|
+
if (first) {
|
|
1429
|
+
const values = first.values;
|
|
1430
|
+
summary.histograms[key] = {
|
|
1431
|
+
count: values.length,
|
|
1432
|
+
sum: values.reduce((a, b) => a + b, 0),
|
|
1433
|
+
min: Math.min(...values),
|
|
1434
|
+
max: Math.max(...values),
|
|
1435
|
+
avg: values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
for (const [key, data] of this.timings) {
|
|
1440
|
+
const first = data[0];
|
|
1441
|
+
if (first) {
|
|
1442
|
+
const values = first.values;
|
|
1443
|
+
summary.timings[key] = {
|
|
1444
|
+
count: values.length,
|
|
1445
|
+
sum: values.reduce((a, b) => a + b, 0),
|
|
1446
|
+
min: Math.min(...values),
|
|
1447
|
+
max: Math.max(...values),
|
|
1448
|
+
avg: values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0,
|
|
1449
|
+
p50: this.percentile(values, 50),
|
|
1450
|
+
p95: this.percentile(values, 95),
|
|
1451
|
+
p99: this.percentile(values, 99)
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
for (const [key, data] of this.sets) {
|
|
1456
|
+
summary.sets[key] = data.size;
|
|
1457
|
+
}
|
|
1458
|
+
return summary;
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Reset all metrics
|
|
1462
|
+
*/
|
|
1463
|
+
reset() {
|
|
1464
|
+
this.counters.clear();
|
|
1465
|
+
this.gauges.clear();
|
|
1466
|
+
this.histograms.clear();
|
|
1467
|
+
this.timings.clear();
|
|
1468
|
+
this.sets.clear();
|
|
1469
|
+
}
|
|
1470
|
+
createKey(name, tags) {
|
|
1471
|
+
const tagStr = Object.entries(tags).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join(",");
|
|
1472
|
+
return tagStr ? `${name}|${tagStr}` : name;
|
|
1473
|
+
}
|
|
1474
|
+
percentile(values, p) {
|
|
1475
|
+
if (values.length === 0) return 0;
|
|
1476
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1477
|
+
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
1478
|
+
const safeIndex = Math.max(0, index);
|
|
1479
|
+
return sorted[safeIndex] ?? 0;
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
var NoopMetrics = class {
|
|
1483
|
+
increment() {
|
|
1484
|
+
}
|
|
1485
|
+
decrement() {
|
|
1486
|
+
}
|
|
1487
|
+
gauge() {
|
|
1488
|
+
}
|
|
1489
|
+
histogram() {
|
|
1490
|
+
}
|
|
1491
|
+
timing() {
|
|
1492
|
+
}
|
|
1493
|
+
startTimer() {
|
|
1494
|
+
return () => {
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
distribution() {
|
|
1498
|
+
}
|
|
1499
|
+
set() {
|
|
1500
|
+
}
|
|
1501
|
+
async flush() {
|
|
1502
|
+
}
|
|
1503
|
+
async close() {
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
// src/factory.ts
|
|
1508
|
+
function createLogger(config) {
|
|
1509
|
+
if (!config.observability.logging) {
|
|
1510
|
+
return new NoopLogger();
|
|
1511
|
+
}
|
|
1512
|
+
return new ConsoleLogger({
|
|
1513
|
+
level: config.observability.logging.level,
|
|
1514
|
+
service: config.observability.tracing.serviceName,
|
|
1515
|
+
environment: config.observability.tracing.environment
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
function createMetrics(config) {
|
|
1519
|
+
if (!config.observability.metrics.enabled) {
|
|
1520
|
+
return new NoopMetrics();
|
|
1521
|
+
}
|
|
1522
|
+
return new MemoryMetrics();
|
|
1523
|
+
}
|
|
1524
|
+
function createPlatform(config) {
|
|
1525
|
+
const finalConfig = config ? deepMerge(loadConfig(), config) : loadConfig();
|
|
1526
|
+
const hasProductionAdapters = finalConfig.database.provider !== "memory" || finalConfig.cache.provider !== "memory" || finalConfig.storage.provider !== "memory" || finalConfig.email.provider !== "memory" && finalConfig.email.provider !== "console" || finalConfig.observability.tracing.provider === "otlp";
|
|
1527
|
+
if (hasProductionAdapters) {
|
|
1528
|
+
console.warn(
|
|
1529
|
+
"createPlatform() is synchronous and cannot initialize production adapters. Use createPlatformAsync() for production adapters, or use memory/console adapters."
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
const db = new MemoryDatabase();
|
|
1533
|
+
const cache = new MemoryCache();
|
|
1534
|
+
const storage = new MemoryStorage();
|
|
1535
|
+
const email = finalConfig.email.provider === "console" ? new ConsoleEmail() : new MemoryEmail();
|
|
1536
|
+
const queue = new MemoryQueue();
|
|
1537
|
+
const logger = createLogger(finalConfig);
|
|
1538
|
+
const metrics = createMetrics(finalConfig);
|
|
1539
|
+
const tracing = finalConfig.observability.tracing.provider === "memory" ? new MemoryTracing() : new NoopTracing();
|
|
1540
|
+
return createPlatformFromAdapters(db, cache, storage, email, queue, logger, metrics, tracing);
|
|
1541
|
+
}
|
|
1542
|
+
function createPlatformFromAdapters(db, cache, storage, email, queue, logger, metrics, tracing) {
|
|
1543
|
+
return {
|
|
1544
|
+
db,
|
|
1545
|
+
cache,
|
|
1546
|
+
storage,
|
|
1547
|
+
email,
|
|
1548
|
+
queue,
|
|
1549
|
+
logger,
|
|
1550
|
+
metrics,
|
|
1551
|
+
tracing,
|
|
1552
|
+
async healthCheck() {
|
|
1553
|
+
const [dbHealth, cacheHealth, storageHealth, emailHealth, queueHealth, tracingHealth] = await Promise.all([
|
|
1554
|
+
db.healthCheck(),
|
|
1555
|
+
cache.healthCheck(),
|
|
1556
|
+
storage.healthCheck(),
|
|
1557
|
+
email.healthCheck(),
|
|
1558
|
+
queue.healthCheck(),
|
|
1559
|
+
tracing.healthCheck()
|
|
1560
|
+
]);
|
|
1561
|
+
return {
|
|
1562
|
+
healthy: dbHealth && cacheHealth && storageHealth && emailHealth && queueHealth && tracingHealth,
|
|
1563
|
+
services: {
|
|
1564
|
+
database: dbHealth,
|
|
1565
|
+
cache: cacheHealth,
|
|
1566
|
+
storage: storageHealth,
|
|
1567
|
+
email: emailHealth,
|
|
1568
|
+
queue: queueHealth,
|
|
1569
|
+
tracing: tracingHealth
|
|
1570
|
+
},
|
|
1571
|
+
timestamp: Date.now()
|
|
1572
|
+
};
|
|
1573
|
+
},
|
|
1574
|
+
async close() {
|
|
1575
|
+
await Promise.all([db.close(), cache.close(), queue.close(), tracing.close()]);
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
function deepMerge(target, source) {
|
|
1580
|
+
const result = { ...target };
|
|
1581
|
+
for (const key in source) {
|
|
1582
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1583
|
+
const sourceValue = source[key];
|
|
1584
|
+
const targetValue = target[key];
|
|
1585
|
+
if (sourceValue !== void 0 && typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue) && typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue)) {
|
|
1586
|
+
result[key] = deepMerge(
|
|
1587
|
+
targetValue,
|
|
1588
|
+
sourceValue
|
|
1589
|
+
);
|
|
1590
|
+
} else if (sourceValue !== void 0) {
|
|
1591
|
+
result[key] = sourceValue;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
return result;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/testing.ts
|
|
1599
|
+
function createTestPlatform() {
|
|
1600
|
+
return createPlatform();
|
|
1601
|
+
}
|
|
1602
|
+
function createTestPlatformWithInternals() {
|
|
1603
|
+
const memoryDb = new MemoryDatabase();
|
|
1604
|
+
const memoryCache = new MemoryCache();
|
|
1605
|
+
const memoryStorage = new MemoryStorage();
|
|
1606
|
+
const memoryEmail = new MemoryEmail();
|
|
1607
|
+
const memoryQueue = new MemoryQueue();
|
|
1608
|
+
const platform = {
|
|
1609
|
+
db: memoryDb,
|
|
1610
|
+
cache: memoryCache,
|
|
1611
|
+
storage: memoryStorage,
|
|
1612
|
+
email: memoryEmail,
|
|
1613
|
+
queue: memoryQueue,
|
|
1614
|
+
async healthCheck() {
|
|
1615
|
+
const [dbHealth, cacheHealth, storageHealth, emailHealth, queueHealth] = await Promise.all([
|
|
1616
|
+
memoryDb.healthCheck(),
|
|
1617
|
+
memoryCache.healthCheck(),
|
|
1618
|
+
memoryStorage.healthCheck(),
|
|
1619
|
+
memoryEmail.healthCheck(),
|
|
1620
|
+
memoryQueue.healthCheck()
|
|
1621
|
+
]);
|
|
1622
|
+
return {
|
|
1623
|
+
healthy: dbHealth && cacheHealth && storageHealth && emailHealth && queueHealth,
|
|
1624
|
+
services: {
|
|
1625
|
+
database: dbHealth,
|
|
1626
|
+
cache: cacheHealth,
|
|
1627
|
+
storage: storageHealth,
|
|
1628
|
+
email: emailHealth,
|
|
1629
|
+
queue: queueHealth
|
|
1630
|
+
},
|
|
1631
|
+
timestamp: Date.now()
|
|
1632
|
+
};
|
|
1633
|
+
},
|
|
1634
|
+
async close() {
|
|
1635
|
+
await Promise.all([memoryDb.close(), memoryCache.close(), memoryQueue.close()]);
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
return {
|
|
1639
|
+
...platform,
|
|
1640
|
+
memoryDb,
|
|
1641
|
+
memoryCache,
|
|
1642
|
+
memoryStorage,
|
|
1643
|
+
memoryEmail,
|
|
1644
|
+
memoryQueue
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
async function seedTestData(db, table, data) {
|
|
1648
|
+
for (const row of data) {
|
|
1649
|
+
await db.from(table).insert(row).execute();
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
async function clearTestPlatform(platform) {
|
|
1653
|
+
platform.memoryDb.clear();
|
|
1654
|
+
platform.memoryCache.clear();
|
|
1655
|
+
platform.memoryStorage.clear();
|
|
1656
|
+
platform.memoryEmail.clear();
|
|
1657
|
+
platform.memoryQueue.clear();
|
|
1658
|
+
}
|
|
1659
|
+
async function waitForQueueDrain(platform, timeoutMs = 5e3) {
|
|
1660
|
+
const startTime = Date.now();
|
|
1661
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1662
|
+
const pendingJobs = platform.memoryQueue.getPendingJobs?.() ?? [];
|
|
1663
|
+
if (pendingJobs.length === 0) {
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1667
|
+
}
|
|
1668
|
+
throw new Error(`Queue did not drain within ${timeoutMs}ms`);
|
|
1669
|
+
}
|
|
1670
|
+
function assertEmailSent(memoryEmail, expected) {
|
|
1671
|
+
const emails = memoryEmail.getSentEmails();
|
|
1672
|
+
const matchesTo = (emailTo, expectedTo) => {
|
|
1673
|
+
if (!emailTo) return false;
|
|
1674
|
+
if (Array.isArray(emailTo)) {
|
|
1675
|
+
return emailTo.some((addr) => addr === expectedTo || typeof addr === "object" && addr.email === expectedTo);
|
|
1676
|
+
}
|
|
1677
|
+
return emailTo === expectedTo || typeof emailTo === "object" && emailTo.email === expectedTo;
|
|
1678
|
+
};
|
|
1679
|
+
const found = emails.find((email) => {
|
|
1680
|
+
if (expected.to && !matchesTo(email.to, expected.to)) return false;
|
|
1681
|
+
if (expected.subject) {
|
|
1682
|
+
if (typeof expected.subject === "string" && email.subject !== expected.subject) return false;
|
|
1683
|
+
if (expected.subject instanceof RegExp && !expected.subject.test(email.subject)) return false;
|
|
1684
|
+
}
|
|
1685
|
+
if (expected.bodyContains) {
|
|
1686
|
+
const body = email.html || email.text || "";
|
|
1687
|
+
if (!body.includes(expected.bodyContains)) return false;
|
|
1688
|
+
}
|
|
1689
|
+
return true;
|
|
1690
|
+
});
|
|
1691
|
+
if (!found) {
|
|
1692
|
+
const emailSummary = emails.map((e) => ` - To: ${JSON.stringify(e.to)}, Subject: ${e.subject}`).join("\n");
|
|
1693
|
+
throw new Error(
|
|
1694
|
+
`Expected email not found.
|
|
1695
|
+
Expected: ${JSON.stringify(expected)}
|
|
1696
|
+
Sent emails:
|
|
1697
|
+
${emailSummary || " (none)"}`
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function createMockFn(implementation) {
|
|
1702
|
+
const calls = [];
|
|
1703
|
+
const fn = function(...args) {
|
|
1704
|
+
calls.push(args);
|
|
1705
|
+
return implementation?.(...args);
|
|
1706
|
+
};
|
|
1707
|
+
Object.defineProperty(fn, "calls", {
|
|
1708
|
+
get: () => calls
|
|
1709
|
+
});
|
|
1710
|
+
Object.defineProperty(fn, "callCount", {
|
|
1711
|
+
get: () => calls.length
|
|
1712
|
+
});
|
|
1713
|
+
Object.defineProperty(fn, "lastCall", {
|
|
1714
|
+
get: () => calls[calls.length - 1]
|
|
1715
|
+
});
|
|
1716
|
+
fn.reset = () => {
|
|
1717
|
+
calls.length = 0;
|
|
1718
|
+
};
|
|
1719
|
+
return fn;
|
|
1720
|
+
}
|
|
1721
|
+
export {
|
|
1722
|
+
ConsoleEmail,
|
|
1723
|
+
ConsoleLogger,
|
|
1724
|
+
EnvSecrets,
|
|
1725
|
+
MemoryCache,
|
|
1726
|
+
MemoryDatabase,
|
|
1727
|
+
MemoryEmail,
|
|
1728
|
+
MemoryMetrics,
|
|
1729
|
+
MemoryQueue,
|
|
1730
|
+
MemorySecrets,
|
|
1731
|
+
MemoryStorage,
|
|
1732
|
+
NoopLogger,
|
|
1733
|
+
NoopMetrics,
|
|
1734
|
+
assertEmailSent,
|
|
1735
|
+
clearTestPlatform,
|
|
1736
|
+
createMockFn,
|
|
1737
|
+
createPlatform,
|
|
1738
|
+
createTestPlatform,
|
|
1739
|
+
createTestPlatformWithInternals,
|
|
1740
|
+
seedTestData,
|
|
1741
|
+
waitForQueueDrain
|
|
1742
|
+
};
|
|
1743
|
+
//# sourceMappingURL=testing.mjs.map
|