@kuckit/infrastructure 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apply-decorators-CW23qWy7.d.ts +23 -0
- package/dist/apply-decorators-CaHIAL5X.js +29 -0
- package/dist/apply-decorators-CaHIAL5X.js.map +1 -0
- package/dist/cache/in-memory-cache-store.d.ts +2 -0
- package/dist/cache/in-memory-cache-store.js +3 -0
- package/dist/cache/index.d.ts +3 -0
- package/dist/cache/index.js +4 -0
- package/dist/cache-BjdZ-Ye4.js +1 -0
- package/dist/core.module-B1TdC0yX.d.ts +17 -0
- package/dist/core.module-DMtIkTcz.js +48 -0
- package/dist/core.module-DMtIkTcz.js.map +1 -0
- package/dist/db/drizzle/db.d.ts +2 -0
- package/dist/db/drizzle/db.js +3 -0
- package/dist/db/drizzle/repositories/index.d.ts +3 -0
- package/dist/db/drizzle/repositories/index.js +4 -0
- package/dist/db/drizzle/repositories/user.drizzle.d.ts +2 -0
- package/dist/db/drizzle/repositories/user.drizzle.js +3 -0
- package/dist/db/transaction.d.ts +2 -0
- package/dist/db/transaction.js +4 -0
- package/dist/db-C4IcCT04.js +25 -0
- package/dist/db-C4IcCT04.js.map +1 -0
- package/dist/db-tAPHBDyL.d.ts +17 -0
- package/dist/decorators/apply-decorators.d.ts +2 -0
- package/dist/decorators/apply-decorators.js +5 -0
- package/dist/decorators/index.d.ts +8 -0
- package/dist/decorators/index.js +9 -0
- package/dist/decorators/use-case-decorator.d.ts +3 -0
- package/dist/decorators/use-case-decorator.js +4 -0
- package/dist/decorators/with-caching.d.ts +2 -0
- package/dist/decorators/with-caching.js +3 -0
- package/dist/decorators/with-rate-limit.d.ts +2 -0
- package/dist/decorators/with-rate-limit.js +3 -0
- package/dist/decorators/with-retry.d.ts +2 -0
- package/dist/decorators/with-retry.js +3 -0
- package/dist/decorators-CqyPE9AQ.js +1 -0
- package/dist/error-handler-BDid7SIZ.d.ts +47 -0
- package/dist/error-handler-D4s_TTI1.js +80 -0
- package/dist/error-handler-D4s_TTI1.js.map +1 -0
- package/dist/errors/error-handler.d.ts +3 -0
- package/dist/errors/error-handler.js +4 -0
- package/dist/errors/index.d.ts +4 -0
- package/dist/errors/index.js +5 -0
- package/dist/errors-BB_jeye8.js +43 -0
- package/dist/errors-BB_jeye8.js.map +1 -0
- package/dist/errors-DfkerzdO.js +1 -0
- package/dist/event-publisher-adapter-B02oKEmP.js +46 -0
- package/dist/event-publisher-adapter-B02oKEmP.js.map +1 -0
- package/dist/event-publisher-adapter-CpxK0OJ3.d.ts +12 -0
- package/dist/events/event-publisher-adapter.d.ts +2 -0
- package/dist/events/event-publisher-adapter.js +3 -0
- package/dist/events/in-memory-event-bus.d.ts +2 -0
- package/dist/events/in-memory-event-bus.js +3 -0
- package/dist/events/in-memory-event-publisher.d.ts +2 -0
- package/dist/events/in-memory-event-publisher.js +3 -0
- package/dist/events/index.d.ts +5 -0
- package/dist/events/index.js +6 -0
- package/dist/events-Dqynhuj2.js +1 -0
- package/dist/in-memory-cache-store-BaRxM--K.d.ts +31 -0
- package/dist/in-memory-cache-store-oClww-8m.js +72 -0
- package/dist/in-memory-cache-store-oClww-8m.js.map +1 -0
- package/dist/in-memory-event-bus-BCyPrNAE.js +60 -0
- package/dist/in-memory-event-bus-BCyPrNAE.js.map +1 -0
- package/dist/in-memory-event-bus-CqIBLRze.d.ts +21 -0
- package/dist/in-memory-event-publisher-BdOlxfkx.js +28 -0
- package/dist/in-memory-event-publisher-BdOlxfkx.js.map +1 -0
- package/dist/in-memory-event-publisher-CxOQ-hnq.d.ts +12 -0
- package/dist/in-memory-rate-limiter-BDSHZXxf.js +72 -0
- package/dist/in-memory-rate-limiter-BDSHZXxf.js.map +1 -0
- package/dist/in-memory-rate-limiter-DJsxdZZR.d.ts +34 -0
- package/dist/index-B5F3AfVc.d.ts +1 -0
- package/dist/index-B7z6dpFd.d.ts +1 -0
- package/dist/index-BH67NKRs.d.ts +2 -0
- package/dist/index-C0yeuOwC.d.ts +1 -0
- package/dist/index-C6nYd7xV.d.ts +2 -0
- package/dist/index-DVGDAddE.d.ts +1 -0
- package/dist/index-DXJbbtWQ.d.ts +1 -0
- package/dist/index-LKrIp3Oo.d.ts +1 -0
- package/dist/index.d.ts +29 -506
- package/dist/index.js +27 -969
- package/dist/logger-Bl10drB8.d.ts +23 -0
- package/dist/logging/index.d.ts +5 -0
- package/dist/logging/index.js +5 -0
- package/dist/logging/request-logger.d.ts +2 -0
- package/dist/logging/request-logger.js +3 -0
- package/dist/logging/structured-logger.d.ts +3 -0
- package/dist/logging/structured-logger.js +3 -0
- package/dist/logging-4mLSrMc6.js +1 -0
- package/dist/modules/core.module.d.ts +10 -0
- package/dist/modules/core.module.js +13 -0
- package/dist/modules/index.d.ts +12 -0
- package/dist/modules/index.js +16 -0
- package/dist/modules/types.d.ts +9 -0
- package/dist/modules/types.js +6 -0
- package/dist/modules/user.module.d.ts +10 -0
- package/dist/modules/user.module.js +4 -0
- package/dist/modules-C_2SF3he.js +1 -0
- package/dist/rate-limiter/in-memory-rate-limiter.d.ts +2 -0
- package/dist/rate-limiter/in-memory-rate-limiter.js +3 -0
- package/dist/rate-limiter/index.d.ts +3 -0
- package/dist/rate-limiter/index.js +4 -0
- package/dist/rate-limiter-BnvPGJOK.js +1 -0
- package/dist/repositories-nTfSJyvW.js +1 -0
- package/dist/request-logger-CK3SOnoz.d.ts +23 -0
- package/dist/request-logger-Cw1XQWTV.js +49 -0
- package/dist/request-logger-Cw1XQWTV.js.map +1 -0
- package/dist/structured-logger-BsxDI9zX.js +119 -0
- package/dist/structured-logger-BsxDI9zX.js.map +1 -0
- package/dist/structured-logger-Dz06Uz-u.d.ts +47 -0
- package/dist/transaction-akuz5Fch.d.ts +22 -0
- package/dist/transaction-r4Sy3eC-.js +35 -0
- package/dist/transaction-r4Sy3eC-.js.map +1 -0
- package/dist/types-65aFqB5L.d.ts +62 -0
- package/dist/use-case-decorator-DzPSPSv5.d.ts +52 -0
- package/dist/use-case-decorator-GmDeYViz.js +118 -0
- package/dist/use-case-decorator-GmDeYViz.js.map +1 -0
- package/dist/user.drizzle-9kkstnkV.d.ts +12 -0
- package/dist/user.drizzle-CgKIqqb3.js +57 -0
- package/dist/user.drizzle-CgKIqqb3.js.map +1 -0
- package/dist/user.module-BEpCbKsU.js +17 -0
- package/dist/user.module-BEpCbKsU.js.map +1 -0
- package/dist/user.module-D3lVJ98T.d.ts +15 -0
- package/dist/with-caching-BniS1aZd.d.ts +39 -0
- package/dist/with-caching-NmBxu7vJ.js +37 -0
- package/dist/with-caching-NmBxu7vJ.js.map +1 -0
- package/dist/with-rate-limit-Cp2V1RHn.js +48 -0
- package/dist/with-rate-limit-Cp2V1RHn.js.map +1 -0
- package/dist/with-rate-limit-DK4ZF-Qg.d.ts +45 -0
- package/dist/with-retry-B9-hUj7I.d.ts +40 -0
- package/dist/with-retry-coyYPiX1.js +49 -0
- package/dist/with-retry-coyYPiX1.js.map +1 -0
- package/package.json +10 -24
- package/src/index.ts +0 -26
package/dist/index.js
CHANGED
|
@@ -1,971 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
1
|
+
import { n as createDbPool, t as createDb } from "./db-C4IcCT04.js";
|
|
2
|
+
import { t as makeDrizzleUserRepository } from "./user.drizzle-CgKIqqb3.js";
|
|
3
|
+
import "./repositories-nTfSJyvW.js";
|
|
4
|
+
import { t as runInTransaction } from "./transaction-r4Sy3eC-.js";
|
|
5
|
+
import { n as makeStructuredLogger, t as StructuredLogger } from "./structured-logger-BsxDI9zX.js";
|
|
6
|
+
import { t as makeRequestLogger } from "./request-logger-Cw1XQWTV.js";
|
|
7
|
+
import "./logging-4mLSrMc6.js";
|
|
8
|
+
import "./errors-BB_jeye8.js";
|
|
9
|
+
import { n as makeErrorHandler, t as ErrorHandler } from "./error-handler-D4s_TTI1.js";
|
|
10
|
+
import "./errors-DfkerzdO.js";
|
|
11
|
+
import { t as InMemoryEventBus } from "./in-memory-event-bus-BCyPrNAE.js";
|
|
12
|
+
import { t as makeEventPublisherAdapter } from "./event-publisher-adapter-B02oKEmP.js";
|
|
13
|
+
import { t as makeInMemoryEventPublisher } from "./in-memory-event-publisher-BdOlxfkx.js";
|
|
14
|
+
import "./events-Dqynhuj2.js";
|
|
15
|
+
import { t as InMemoryCacheStore } from "./in-memory-cache-store-oClww-8m.js";
|
|
16
|
+
import "./cache-BjdZ-Ye4.js";
|
|
17
|
+
import { t as InMemoryRateLimiterStore } from "./in-memory-rate-limiter-BDSHZXxf.js";
|
|
18
|
+
import "./rate-limiter-BnvPGJOK.js";
|
|
19
|
+
import { a as withRequestTracing, i as withPerformanceMonitoring, n as makeDecoratedUseCase, r as withErrorHandling, t as decorateUseCase } from "./use-case-decorator-GmDeYViz.js";
|
|
20
|
+
import { t as applyDecorators } from "./apply-decorators-CaHIAL5X.js";
|
|
21
|
+
import { t as withCaching } from "./with-caching-NmBxu7vJ.js";
|
|
22
|
+
import { t as withRetry } from "./with-retry-coyYPiX1.js";
|
|
23
|
+
import { n as withRateLimit, t as RateLimitError } from "./with-rate-limit-Cp2V1RHn.js";
|
|
24
|
+
import "./decorators-CqyPE9AQ.js";
|
|
25
|
+
import { t as registerCoreModule } from "./core.module-DMtIkTcz.js";
|
|
26
|
+
import { t as registerUserModule } from "./user.module-BEpCbKsU.js";
|
|
27
|
+
import "./modules-C_2SF3he.js";
|
|
13
28
|
|
|
14
|
-
//#region src/db/drizzle/db.ts
|
|
15
|
-
/**
|
|
16
|
-
* Create database connection pool
|
|
17
|
-
*/
|
|
18
|
-
const createDbPool = (connectionString) => {
|
|
19
|
-
return new Pool({
|
|
20
|
-
connectionString,
|
|
21
|
-
max: 20,
|
|
22
|
-
idleTimeoutMillis: 3e4,
|
|
23
|
-
connectionTimeoutMillis: 2e3
|
|
24
|
-
});
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Create Drizzle instance from pool
|
|
28
|
-
*/
|
|
29
|
-
const createDb = (pool) => {
|
|
30
|
-
return drizzle(pool);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
//#endregion
|
|
34
|
-
//#region src/db/drizzle/repositories/user.drizzle.ts
|
|
35
|
-
/**
|
|
36
|
-
* Map Drizzle row to domain entity
|
|
37
|
-
*/
|
|
38
|
-
const toDomainUser = (row) => ({
|
|
39
|
-
id: row.id,
|
|
40
|
-
name: row.name,
|
|
41
|
-
email: row.email,
|
|
42
|
-
emailVerified: row.emailVerified,
|
|
43
|
-
permissions: row.permissions,
|
|
44
|
-
createdAt: row.createdAt,
|
|
45
|
-
updatedAt: row.updatedAt
|
|
46
|
-
});
|
|
47
|
-
/**
|
|
48
|
-
* Map domain entity to Drizzle insert type
|
|
49
|
-
*/
|
|
50
|
-
const toDbUser = (user$1) => ({
|
|
51
|
-
id: user$1.id,
|
|
52
|
-
name: user$1.name,
|
|
53
|
-
email: user$1.email,
|
|
54
|
-
emailVerified: user$1.emailVerified,
|
|
55
|
-
permissions: [...user$1.permissions],
|
|
56
|
-
createdAt: user$1.createdAt,
|
|
57
|
-
updatedAt: user$1.updatedAt
|
|
58
|
-
});
|
|
59
|
-
/**
|
|
60
|
-
* Drizzle implementation of UserRepository
|
|
61
|
-
*/
|
|
62
|
-
const makeDrizzleUserRepository = (db) => ({
|
|
63
|
-
async findById(id) {
|
|
64
|
-
const rows = await db.select().from(user).where(eq(user.id, id)).limit(1);
|
|
65
|
-
return rows[0] ? toDomainUser(rows[0]) : null;
|
|
66
|
-
},
|
|
67
|
-
async findByEmail(email) {
|
|
68
|
-
const rows = await db.select().from(user).where(eq(user.email, email)).limit(1);
|
|
69
|
-
return rows[0] ? toDomainUser(rows[0]) : null;
|
|
70
|
-
},
|
|
71
|
-
async save(user$1) {
|
|
72
|
-
await db.insert(user).values(toDbUser(user$1)).onConflictDoUpdate({
|
|
73
|
-
target: user.id,
|
|
74
|
-
set: {
|
|
75
|
-
name: user$1.name,
|
|
76
|
-
email: user$1.email,
|
|
77
|
-
emailVerified: user$1.emailVerified,
|
|
78
|
-
permissions: [...user$1.permissions],
|
|
79
|
-
updatedAt: user$1.updatedAt
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
//#endregion
|
|
86
|
-
//#region src/db/transaction.ts
|
|
87
|
-
/**
|
|
88
|
-
* Run a function within a database transaction
|
|
89
|
-
* Creates a child scope with transaction-bound repositories
|
|
90
|
-
*
|
|
91
|
-
* Usage:
|
|
92
|
-
* ```typescript
|
|
93
|
-
* const result = await runInTransaction(container, async (txScope) => {
|
|
94
|
-
* const { userRepository } = txScope.cradle
|
|
95
|
-
* await userRepository.save(user)
|
|
96
|
-
* return user
|
|
97
|
-
* })
|
|
98
|
-
* ```
|
|
99
|
-
*/
|
|
100
|
-
const runInTransaction = async (container, fn) => {
|
|
101
|
-
return container.resolve("db").transaction(async (tx) => {
|
|
102
|
-
const txScope = container.createScope();
|
|
103
|
-
txScope.register({
|
|
104
|
-
db: asValue(tx),
|
|
105
|
-
userRepository: asFunction(({ db }) => makeDrizzleUserRepository(db)).scoped()
|
|
106
|
-
});
|
|
107
|
-
try {
|
|
108
|
-
return await fn(txScope);
|
|
109
|
-
} finally {
|
|
110
|
-
txScope.dispose();
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
//#endregion
|
|
116
|
-
//#region src/logging/structured-logger.ts
|
|
117
|
-
/**
|
|
118
|
-
* Structured logger implementation
|
|
119
|
-
* - JSON logs compatible with Loki
|
|
120
|
-
* - File + stdout support
|
|
121
|
-
* - Prometheus metrics collection
|
|
122
|
-
*/
|
|
123
|
-
var StructuredLogger = class {
|
|
124
|
-
fileStream = null;
|
|
125
|
-
metrics;
|
|
126
|
-
constructor(options = {}) {
|
|
127
|
-
this.options = options;
|
|
128
|
-
this.metrics = new MetricsCollector();
|
|
129
|
-
if (options.enableFile && options.logDir) this.initFileStream(options.logDir);
|
|
130
|
-
}
|
|
131
|
-
initFileStream(logDir) {
|
|
132
|
-
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
133
|
-
this.fileStream = createWriteStream(path.join(logDir, `app-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`), { flags: "a" });
|
|
134
|
-
}
|
|
135
|
-
debug(message, meta) {
|
|
136
|
-
this.log("DEBUG", message, meta);
|
|
137
|
-
}
|
|
138
|
-
info(message, meta) {
|
|
139
|
-
this.log("INFO", message, meta);
|
|
140
|
-
}
|
|
141
|
-
warn(message, meta) {
|
|
142
|
-
this.log("WARN", message, meta);
|
|
143
|
-
}
|
|
144
|
-
error(message, meta) {
|
|
145
|
-
const context = meta instanceof Error ? {
|
|
146
|
-
error: meta.message,
|
|
147
|
-
stack: meta.stack
|
|
148
|
-
} : meta;
|
|
149
|
-
this.log("ERROR", message, context);
|
|
150
|
-
}
|
|
151
|
-
log(level, message, meta) {
|
|
152
|
-
if (this.shouldSkip(level)) return;
|
|
153
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
154
|
-
const logEntry = this.formatLog(level, message, meta, timestamp);
|
|
155
|
-
console.log(logEntry);
|
|
156
|
-
if (this.fileStream) this.fileStream.write(logEntry + "\n");
|
|
157
|
-
this.metrics.recordLog(level, meta?.feature);
|
|
158
|
-
}
|
|
159
|
-
shouldSkip(level) {
|
|
160
|
-
const levels = [
|
|
161
|
-
"DEBUG",
|
|
162
|
-
"INFO",
|
|
163
|
-
"WARN",
|
|
164
|
-
"ERROR"
|
|
165
|
-
];
|
|
166
|
-
const minLevel = this.options.minLevel || "DEBUG";
|
|
167
|
-
return levels.indexOf(level) < levels.indexOf(minLevel);
|
|
168
|
-
}
|
|
169
|
-
formatLog(level, message, meta, timestamp) {
|
|
170
|
-
const logObject = {
|
|
171
|
-
timestamp,
|
|
172
|
-
level,
|
|
173
|
-
message,
|
|
174
|
-
...meta
|
|
175
|
-
};
|
|
176
|
-
if (meta?.requestId) logObject.requestId = meta.requestId;
|
|
177
|
-
if (meta?.userId) logObject.userId = meta.userId;
|
|
178
|
-
if (meta?.feature) logObject.feature = meta.feature;
|
|
179
|
-
return JSON.stringify(logObject);
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Get metrics in Prometheus text format
|
|
183
|
-
*/
|
|
184
|
-
getMetrics() {
|
|
185
|
-
return this.metrics.toPrometheusFormat();
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Cleanup resources
|
|
189
|
-
*/
|
|
190
|
-
async dispose() {
|
|
191
|
-
return new Promise((resolve) => {
|
|
192
|
-
if (this.fileStream) this.fileStream.end(() => resolve());
|
|
193
|
-
else resolve();
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
/**
|
|
198
|
-
* Prometheus metrics collector
|
|
199
|
-
*/
|
|
200
|
-
var MetricsCollector = class {
|
|
201
|
-
logCount = new Map([
|
|
202
|
-
["DEBUG", 0],
|
|
203
|
-
["INFO", 0],
|
|
204
|
-
["WARN", 0],
|
|
205
|
-
["ERROR", 0]
|
|
206
|
-
]);
|
|
207
|
-
featureCount = /* @__PURE__ */ new Map();
|
|
208
|
-
recordLog(level, feature) {
|
|
209
|
-
this.logCount.set(level, (this.logCount.get(level) || 0) + 1);
|
|
210
|
-
if (feature) this.featureCount.set(feature, (this.featureCount.get(feature) || 0) + 1);
|
|
211
|
-
}
|
|
212
|
-
toPrometheusFormat() {
|
|
213
|
-
const lines = ["# HELP app_logs_total Total number of logs by level", "# TYPE app_logs_total counter"];
|
|
214
|
-
for (const [level, count] of this.logCount) lines.push(`app_logs_total{level="${level}"} ${count}`);
|
|
215
|
-
lines.push("");
|
|
216
|
-
lines.push("# HELP app_feature_logs_total Total logs by feature");
|
|
217
|
-
lines.push("# TYPE app_feature_logs_total counter");
|
|
218
|
-
for (const [feature, count] of this.featureCount) lines.push(`app_feature_logs_total{feature="${feature}"} ${count}`);
|
|
219
|
-
return lines.join("\n");
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
/**
|
|
223
|
-
* Create logger instance
|
|
224
|
-
*/
|
|
225
|
-
const makeStructuredLogger = (options) => {
|
|
226
|
-
return new StructuredLogger(options);
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
//#endregion
|
|
230
|
-
//#region src/logging/request-logger.ts
|
|
231
|
-
/**
|
|
232
|
-
* Create a request-scoped logger that automatically includes
|
|
233
|
-
* requestId and userId in all log entries.
|
|
234
|
-
*
|
|
235
|
-
* This wraps the base singleton logger and enriches all log
|
|
236
|
-
* calls with request context for Loki-compatible structured logging.
|
|
237
|
-
*/
|
|
238
|
-
const makeRequestLogger = (options) => {
|
|
239
|
-
const context = {
|
|
240
|
-
requestId: options.requestId,
|
|
241
|
-
...options.userId && { userId: options.userId }
|
|
242
|
-
};
|
|
243
|
-
return {
|
|
244
|
-
debug: (message, meta) => {
|
|
245
|
-
options.baseLogger.debug(message, {
|
|
246
|
-
...context,
|
|
247
|
-
...meta
|
|
248
|
-
});
|
|
249
|
-
},
|
|
250
|
-
info: (message, meta) => {
|
|
251
|
-
options.baseLogger.info(message, {
|
|
252
|
-
...context,
|
|
253
|
-
...meta
|
|
254
|
-
});
|
|
255
|
-
},
|
|
256
|
-
warn: (message, meta) => {
|
|
257
|
-
options.baseLogger.warn(message, {
|
|
258
|
-
...context,
|
|
259
|
-
...meta
|
|
260
|
-
});
|
|
261
|
-
},
|
|
262
|
-
error: (message, meta) => {
|
|
263
|
-
if (meta instanceof Error) options.baseLogger.error(message, {
|
|
264
|
-
...context,
|
|
265
|
-
error: meta.message,
|
|
266
|
-
stack: meta.stack
|
|
267
|
-
});
|
|
268
|
-
else options.baseLogger.error(message, {
|
|
269
|
-
...context,
|
|
270
|
-
...meta
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
//#endregion
|
|
277
|
-
//#region ../domain/src/errors/index.ts
|
|
278
|
-
/**
|
|
279
|
-
* Severity levels for error classification
|
|
280
|
-
*/
|
|
281
|
-
let ErrorSeverity = /* @__PURE__ */ function(ErrorSeverity$1) {
|
|
282
|
-
ErrorSeverity$1["LOW"] = "LOW";
|
|
283
|
-
ErrorSeverity$1["MEDIUM"] = "MEDIUM";
|
|
284
|
-
ErrorSeverity$1["HIGH"] = "HIGH";
|
|
285
|
-
ErrorSeverity$1["CRITICAL"] = "CRITICAL";
|
|
286
|
-
return ErrorSeverity$1;
|
|
287
|
-
}({});
|
|
288
|
-
/**
|
|
289
|
-
* Application error base class
|
|
290
|
-
*/
|
|
291
|
-
var AppError$1 = class AppError$1 extends Error {
|
|
292
|
-
code;
|
|
293
|
-
severity;
|
|
294
|
-
statusCode;
|
|
295
|
-
meta;
|
|
296
|
-
constructor(code, message, options = {}) {
|
|
297
|
-
super(message);
|
|
298
|
-
this.name = "AppError";
|
|
299
|
-
this.code = code;
|
|
300
|
-
this.severity = options.severity || ErrorSeverity.MEDIUM;
|
|
301
|
-
this.statusCode = options.statusCode || 500;
|
|
302
|
-
this.meta = options.meta || {};
|
|
303
|
-
Object.setPrototypeOf(this, AppError$1.prototype);
|
|
304
|
-
}
|
|
305
|
-
toJSON() {
|
|
306
|
-
return {
|
|
307
|
-
name: this.name,
|
|
308
|
-
code: this.code,
|
|
309
|
-
message: this.message,
|
|
310
|
-
severity: this.severity,
|
|
311
|
-
statusCode: this.statusCode,
|
|
312
|
-
meta: this.meta
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
//#endregion
|
|
318
|
-
//#region src/errors/error-handler.ts
|
|
319
|
-
/**
|
|
320
|
-
* Error handler for structured error logging and serialization
|
|
321
|
-
*/
|
|
322
|
-
var ErrorHandler = class ErrorHandler {
|
|
323
|
-
constructor(logger) {
|
|
324
|
-
this.logger = logger;
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Handle error and serialize for API response
|
|
328
|
-
*/
|
|
329
|
-
handle(error, context) {
|
|
330
|
-
const appError = this.normalize(error);
|
|
331
|
-
this.log(appError, context);
|
|
332
|
-
return this.serialize(appError);
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Normalize any error to AppError
|
|
336
|
-
*/
|
|
337
|
-
normalize(error) {
|
|
338
|
-
if (error instanceof AppError$1) return error;
|
|
339
|
-
if (error instanceof Error) return new AppError$1("INTERNAL_SERVER_ERROR", error.message, {
|
|
340
|
-
statusCode: 500,
|
|
341
|
-
meta: { originalError: error.name }
|
|
342
|
-
});
|
|
343
|
-
return new AppError$1("INTERNAL_SERVER_ERROR", "Unknown error occurred", { statusCode: 500 });
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Log error with appropriate level
|
|
347
|
-
*/
|
|
348
|
-
log(error, context) {
|
|
349
|
-
const logContext = {
|
|
350
|
-
code: error.code,
|
|
351
|
-
severity: error.severity,
|
|
352
|
-
stack: error.stack,
|
|
353
|
-
...context
|
|
354
|
-
};
|
|
355
|
-
switch (error.severity) {
|
|
356
|
-
case "LOW":
|
|
357
|
-
this.logger.debug(error.message, logContext);
|
|
358
|
-
break;
|
|
359
|
-
case "MEDIUM":
|
|
360
|
-
this.logger.warn(error.message, logContext);
|
|
361
|
-
break;
|
|
362
|
-
case "HIGH":
|
|
363
|
-
case "CRITICAL":
|
|
364
|
-
this.logger.error(error.message, logContext);
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Serialize error for API response
|
|
370
|
-
*/
|
|
371
|
-
serialize(error) {
|
|
372
|
-
return {
|
|
373
|
-
code: error.code,
|
|
374
|
-
message: error.message,
|
|
375
|
-
statusCode: error.statusCode,
|
|
376
|
-
...Object.keys(error.meta).length > 0 && { meta: error.meta }
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Create error handler factory
|
|
381
|
-
*/
|
|
382
|
-
static create(logger) {
|
|
383
|
-
return new ErrorHandler(logger);
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
/**
|
|
387
|
-
* Factory for error handler
|
|
388
|
-
*/
|
|
389
|
-
const makeErrorHandler = (logger) => {
|
|
390
|
-
return ErrorHandler.create(logger);
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
//#endregion
|
|
394
|
-
//#region src/events/in-memory-event-bus.ts
|
|
395
|
-
/**
|
|
396
|
-
* In-memory event bus
|
|
397
|
-
* Stores handlers in Sets, executes synchronously or asynchronously
|
|
398
|
-
*/
|
|
399
|
-
var InMemoryEventBus = class {
|
|
400
|
-
handlers = /* @__PURE__ */ new Map();
|
|
401
|
-
allHandlers = /* @__PURE__ */ new Set();
|
|
402
|
-
constructor(logger) {
|
|
403
|
-
this.logger = logger;
|
|
404
|
-
}
|
|
405
|
-
async publish(event) {
|
|
406
|
-
const eventName = String(event.name);
|
|
407
|
-
const handlers = this.handlers.get(eventName) ?? /* @__PURE__ */ new Set();
|
|
408
|
-
const allHandlers = Array.from(this.allHandlers);
|
|
409
|
-
const eventHandlers = [...handlers, ...allHandlers];
|
|
410
|
-
const failed = (await Promise.allSettled(eventHandlers.map((handler) => Promise.resolve(handler(event)).catch((error) => {
|
|
411
|
-
this.logger.error("Error in event handler", {
|
|
412
|
-
eventName,
|
|
413
|
-
eventId: event.meta.eventId,
|
|
414
|
-
error
|
|
415
|
-
});
|
|
416
|
-
})))).filter((r) => r.status === "rejected").length;
|
|
417
|
-
if (failed > 0) this.logger.warn(`Event published with ${failed} handler errors`, {
|
|
418
|
-
eventName,
|
|
419
|
-
eventId: event.meta.eventId
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
publishAsync(event) {
|
|
423
|
-
queueMicrotask(() => {
|
|
424
|
-
this.publish(event).catch((error) => {
|
|
425
|
-
this.logger.error("Unhandled error in async event publishing", {
|
|
426
|
-
eventName: event.name,
|
|
427
|
-
eventId: event.meta.eventId,
|
|
428
|
-
error
|
|
429
|
-
});
|
|
430
|
-
});
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
subscribe(eventName, handler) {
|
|
434
|
-
const eventNameStr = String(eventName);
|
|
435
|
-
if (!this.handlers.has(eventNameStr)) this.handlers.set(eventNameStr, /* @__PURE__ */ new Set());
|
|
436
|
-
const handlerSet = this.handlers.get(eventNameStr);
|
|
437
|
-
handlerSet.add(handler);
|
|
438
|
-
return () => {
|
|
439
|
-
handlerSet.delete(handler);
|
|
440
|
-
if (handlerSet.size === 0) this.handlers.delete(eventNameStr);
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
subscribeAll(handler) {
|
|
444
|
-
this.allHandlers.add(handler);
|
|
445
|
-
return () => {
|
|
446
|
-
this.allHandlers.delete(handler);
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
//#endregion
|
|
452
|
-
//#region src/events/event-publisher-adapter.ts
|
|
453
|
-
/**
|
|
454
|
-
* Adapter that implements EventPublisher port using EventBus
|
|
455
|
-
* Converts the simplified EventPublisher interface to full EventBus types
|
|
456
|
-
*/
|
|
457
|
-
const makeEventPublisherAdapter = (eventBus, logger) => {
|
|
458
|
-
return {
|
|
459
|
-
async publish(eventName, payload, aggregateId) {
|
|
460
|
-
const event = {
|
|
461
|
-
name: eventName,
|
|
462
|
-
payload,
|
|
463
|
-
meta: {
|
|
464
|
-
eventId: randomUUID(),
|
|
465
|
-
occurredAt: /* @__PURE__ */ new Date(),
|
|
466
|
-
version: 1,
|
|
467
|
-
aggregateId,
|
|
468
|
-
correlationId: randomUUID()
|
|
469
|
-
}
|
|
470
|
-
};
|
|
471
|
-
logger.debug("Publishing event", {
|
|
472
|
-
eventName,
|
|
473
|
-
eventId: event.meta.eventId,
|
|
474
|
-
aggregateId
|
|
475
|
-
});
|
|
476
|
-
await eventBus.publish(event);
|
|
477
|
-
},
|
|
478
|
-
subscribe(eventName, handler) {
|
|
479
|
-
return eventBus.subscribe(eventName, async (event) => {
|
|
480
|
-
try {
|
|
481
|
-
await handler(event.payload);
|
|
482
|
-
} catch (error) {
|
|
483
|
-
logger.error("Error in event handler", {
|
|
484
|
-
eventName,
|
|
485
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
//#endregion
|
|
494
|
-
//#region src/events/in-memory-event-publisher.ts
|
|
495
|
-
/**
|
|
496
|
-
* In-memory event publisher implementation
|
|
497
|
-
* Suitable for development and testing
|
|
498
|
-
*/
|
|
499
|
-
const makeInMemoryEventPublisher = () => {
|
|
500
|
-
const subscribers = /* @__PURE__ */ new Map();
|
|
501
|
-
return {
|
|
502
|
-
async publish(eventName, payload, _aggregateId) {
|
|
503
|
-
const handlers = subscribers.get(eventName);
|
|
504
|
-
if (!handlers) return;
|
|
505
|
-
await Promise.all(Array.from(handlers).map((handler) => Promise.resolve(handler(payload))));
|
|
506
|
-
},
|
|
507
|
-
subscribe(eventName, handler) {
|
|
508
|
-
if (!subscribers.has(eventName)) subscribers.set(eventName, /* @__PURE__ */ new Set());
|
|
509
|
-
const handlers = subscribers.get(eventName);
|
|
510
|
-
handlers.add(handler);
|
|
511
|
-
return () => {
|
|
512
|
-
handlers.delete(handler);
|
|
513
|
-
if (handlers.size === 0) subscribers.delete(eventName);
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
//#endregion
|
|
520
|
-
//#region src/cache/in-memory-cache-store.ts
|
|
521
|
-
/**
|
|
522
|
-
* In-memory cache store implementation
|
|
523
|
-
* Suitable for single-process applications or development
|
|
524
|
-
* For distributed systems, use Redis adapter
|
|
525
|
-
*/
|
|
526
|
-
var InMemoryCacheStore = class {
|
|
527
|
-
cache = /* @__PURE__ */ new Map();
|
|
528
|
-
cleanupTimer = null;
|
|
529
|
-
constructor(cleanupIntervalMs = 6e4) {
|
|
530
|
-
this.cleanupIntervalMs = cleanupIntervalMs;
|
|
531
|
-
this.startCleanup();
|
|
532
|
-
}
|
|
533
|
-
async get(key) {
|
|
534
|
-
const entry = this.cache.get(key);
|
|
535
|
-
if (!entry) return;
|
|
536
|
-
if (entry.expiresAt && entry.expiresAt < Date.now()) {
|
|
537
|
-
this.cache.delete(key);
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
return entry.value;
|
|
541
|
-
}
|
|
542
|
-
async set(key, value, ttlMs) {
|
|
543
|
-
const entry = {
|
|
544
|
-
value,
|
|
545
|
-
expiresAt: ttlMs ? Date.now() + ttlMs : void 0
|
|
546
|
-
};
|
|
547
|
-
this.cache.set(key, entry);
|
|
548
|
-
}
|
|
549
|
-
async del(key) {
|
|
550
|
-
this.cache.delete(key);
|
|
551
|
-
}
|
|
552
|
-
async clear() {
|
|
553
|
-
this.cache.clear();
|
|
554
|
-
}
|
|
555
|
-
async has(key) {
|
|
556
|
-
const entry = this.cache.get(key);
|
|
557
|
-
if (!entry) return false;
|
|
558
|
-
if (entry.expiresAt && entry.expiresAt < Date.now()) {
|
|
559
|
-
this.cache.delete(key);
|
|
560
|
-
return false;
|
|
561
|
-
}
|
|
562
|
-
return true;
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Start background cleanup of expired entries
|
|
566
|
-
*/
|
|
567
|
-
startCleanup() {
|
|
568
|
-
this.cleanupTimer = setInterval(() => {
|
|
569
|
-
const now = Date.now();
|
|
570
|
-
let expired = 0;
|
|
571
|
-
for (const [key, entry] of this.cache.entries()) if (entry.expiresAt && entry.expiresAt < now) {
|
|
572
|
-
this.cache.delete(key);
|
|
573
|
-
expired++;
|
|
574
|
-
}
|
|
575
|
-
if (expired > 0) {}
|
|
576
|
-
}, this.cleanupIntervalMs);
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Stop cleanup timer
|
|
580
|
-
*/
|
|
581
|
-
destroy() {
|
|
582
|
-
if (this.cleanupTimer) {
|
|
583
|
-
clearInterval(this.cleanupTimer);
|
|
584
|
-
this.cleanupTimer = null;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
//#endregion
|
|
590
|
-
//#region src/rate-limiter/in-memory-rate-limiter.ts
|
|
591
|
-
/**
|
|
592
|
-
* In-memory rate limiter using token bucket algorithm
|
|
593
|
-
* Suitable for single-process applications or development
|
|
594
|
-
* For distributed systems, use Redis adapter
|
|
595
|
-
*/
|
|
596
|
-
var InMemoryRateLimiterStore = class {
|
|
597
|
-
buckets = /* @__PURE__ */ new Map();
|
|
598
|
-
cleanupTimer = null;
|
|
599
|
-
constructor(cleanupIntervalMs = 6e4) {
|
|
600
|
-
this.cleanupIntervalMs = cleanupIntervalMs;
|
|
601
|
-
this.startCleanup();
|
|
602
|
-
}
|
|
603
|
-
async checkLimit(key, capacity, refillPerSecond) {
|
|
604
|
-
const now = Date.now();
|
|
605
|
-
let bucket = this.buckets.get(key);
|
|
606
|
-
if (!bucket) {
|
|
607
|
-
bucket = {
|
|
608
|
-
tokens: capacity,
|
|
609
|
-
lastRefillAt: now
|
|
610
|
-
};
|
|
611
|
-
this.buckets.set(key, bucket);
|
|
612
|
-
}
|
|
613
|
-
const tokensToAdd = (now - bucket.lastRefillAt) / 1e3 * refillPerSecond;
|
|
614
|
-
bucket.tokens = Math.min(capacity, bucket.tokens + tokensToAdd);
|
|
615
|
-
bucket.lastRefillAt = now;
|
|
616
|
-
const allowed = bucket.tokens >= 1;
|
|
617
|
-
if (allowed) bucket.tokens -= 1;
|
|
618
|
-
const secondsToReset = Math.max(0, 1 - bucket.tokens) / refillPerSecond;
|
|
619
|
-
const resetAt = new Date(now + secondsToReset * 1e3);
|
|
620
|
-
return {
|
|
621
|
-
allowed,
|
|
622
|
-
tokensRemaining: Math.floor(bucket.tokens),
|
|
623
|
-
resetAt
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
async reset(key) {
|
|
627
|
-
this.buckets.delete(key);
|
|
628
|
-
}
|
|
629
|
-
async clear() {
|
|
630
|
-
this.buckets.clear();
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Start background cleanup of old buckets
|
|
634
|
-
* Removes buckets that haven't been used in a long time
|
|
635
|
-
*/
|
|
636
|
-
startCleanup() {
|
|
637
|
-
this.cleanupTimer = setInterval(() => {
|
|
638
|
-
const now = Date.now();
|
|
639
|
-
const maxAgeMs = 36e5;
|
|
640
|
-
let removed = 0;
|
|
641
|
-
for (const [key, bucket] of this.buckets.entries()) if (now - bucket.lastRefillAt > maxAgeMs) {
|
|
642
|
-
this.buckets.delete(key);
|
|
643
|
-
removed++;
|
|
644
|
-
}
|
|
645
|
-
if (removed > 0) {}
|
|
646
|
-
}, this.cleanupIntervalMs);
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* Stop cleanup timer
|
|
650
|
-
*/
|
|
651
|
-
destroy() {
|
|
652
|
-
if (this.cleanupTimer) {
|
|
653
|
-
clearInterval(this.cleanupTimer);
|
|
654
|
-
this.cleanupTimer = null;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
//#endregion
|
|
660
|
-
//#region src/decorators/use-case-decorator.ts
|
|
661
|
-
/**
|
|
662
|
-
* Base decorator that adds performance monitoring
|
|
663
|
-
*/
|
|
664
|
-
const withPerformanceMonitoring = (useCase, context, featureName) => {
|
|
665
|
-
return (async (...args) => {
|
|
666
|
-
const startTime = Date.now();
|
|
667
|
-
try {
|
|
668
|
-
const result = await useCase(...args);
|
|
669
|
-
const durationMs = Date.now() - startTime;
|
|
670
|
-
context.logger.debug(`${featureName}_completed`, {
|
|
671
|
-
requestId: context.requestId,
|
|
672
|
-
feature: featureName,
|
|
673
|
-
durationMs
|
|
674
|
-
});
|
|
675
|
-
return result;
|
|
676
|
-
} catch (error) {
|
|
677
|
-
const durationMs = Date.now() - startTime;
|
|
678
|
-
if (error instanceof AppError$1) context.logger.debug(`${featureName}_error`, {
|
|
679
|
-
requestId: context.requestId,
|
|
680
|
-
feature: featureName,
|
|
681
|
-
durationMs,
|
|
682
|
-
code: error.code,
|
|
683
|
-
severity: error.severity
|
|
684
|
-
});
|
|
685
|
-
throw error;
|
|
686
|
-
}
|
|
687
|
-
});
|
|
688
|
-
};
|
|
689
|
-
/**
|
|
690
|
-
* Base decorator that adds error handling and conversion
|
|
691
|
-
*/
|
|
692
|
-
const withErrorHandling = (useCase, context, featureName) => {
|
|
693
|
-
return (async (...args) => {
|
|
694
|
-
try {
|
|
695
|
-
return await useCase(...args);
|
|
696
|
-
} catch (error) {
|
|
697
|
-
if (error instanceof AppError$1) throw error;
|
|
698
|
-
if (error instanceof Error) {
|
|
699
|
-
context.logger.error(`${featureName}_unexpected_error`, {
|
|
700
|
-
requestId: context.requestId,
|
|
701
|
-
feature: featureName,
|
|
702
|
-
errorName: error.name,
|
|
703
|
-
errorMessage: error.message,
|
|
704
|
-
stack: error.stack
|
|
705
|
-
});
|
|
706
|
-
throw new AppError$1("INTERNAL_SERVER_ERROR", `Unexpected error in ${featureName}`, {
|
|
707
|
-
statusCode: 500,
|
|
708
|
-
meta: {
|
|
709
|
-
originalError: error.message,
|
|
710
|
-
feature: featureName
|
|
711
|
-
}
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
context.logger.error(`${featureName}_unknown_error`, {
|
|
715
|
-
requestId: context.requestId,
|
|
716
|
-
feature: featureName
|
|
717
|
-
});
|
|
718
|
-
throw new AppError$1("INTERNAL_SERVER_ERROR", `Unknown error in ${featureName}`, {
|
|
719
|
-
statusCode: 500,
|
|
720
|
-
meta: { feature: featureName }
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
});
|
|
724
|
-
};
|
|
725
|
-
/**
|
|
726
|
-
* Request tracing decorator - adds context to logs
|
|
727
|
-
*/
|
|
728
|
-
const withRequestTracing = (useCase, context, featureName) => {
|
|
729
|
-
return (async (...args) => {
|
|
730
|
-
context.logger.info(`${featureName}_started`, {
|
|
731
|
-
requestId: context.requestId,
|
|
732
|
-
userId: context.userId,
|
|
733
|
-
feature: featureName
|
|
734
|
-
});
|
|
735
|
-
try {
|
|
736
|
-
const result = await useCase(...args);
|
|
737
|
-
context.logger.info(`${featureName}_success`, {
|
|
738
|
-
requestId: context.requestId,
|
|
739
|
-
userId: context.userId,
|
|
740
|
-
feature: featureName
|
|
741
|
-
});
|
|
742
|
-
return result;
|
|
743
|
-
} catch (error) {
|
|
744
|
-
context.logger.error(`${featureName}_failed`, {
|
|
745
|
-
requestId: context.requestId,
|
|
746
|
-
userId: context.userId,
|
|
747
|
-
feature: featureName,
|
|
748
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
749
|
-
});
|
|
750
|
-
throw error;
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
};
|
|
754
|
-
/**
|
|
755
|
-
* Compose multiple decorators together
|
|
756
|
-
* Order: Tracing → Performance → Error Handling
|
|
757
|
-
* (outermost → innermost)
|
|
758
|
-
*/
|
|
759
|
-
const decorateUseCase = (useCase, context, featureName) => {
|
|
760
|
-
let decorated = withErrorHandling(useCase, context, featureName);
|
|
761
|
-
decorated = withPerformanceMonitoring(decorated, context, featureName);
|
|
762
|
-
decorated = withRequestTracing(decorated, context, featureName);
|
|
763
|
-
return decorated;
|
|
764
|
-
};
|
|
765
|
-
/**
|
|
766
|
-
* Factory for creating decorated use cases
|
|
767
|
-
* Used in dependency injection to wrap use cases automatically
|
|
768
|
-
*/
|
|
769
|
-
const makeDecoratedUseCase = (useCase, context, featureName) => {
|
|
770
|
-
return decorateUseCase(useCase, context, featureName);
|
|
771
|
-
};
|
|
772
|
-
|
|
773
|
-
//#endregion
|
|
774
|
-
//#region src/decorators/apply-decorators.ts
|
|
775
|
-
/**
|
|
776
|
-
* Helper to apply decorators to use cases in module registration
|
|
777
|
-
* Reduces boilerplate in feature modules
|
|
778
|
-
*
|
|
779
|
-
* Usage:
|
|
780
|
-
* ```
|
|
781
|
-
* getUserProfile: asFunction(({ userRepository, ...deps }) =>
|
|
782
|
-
* applyDecorators(
|
|
783
|
-
* makeGetUserProfile({ userRepository }),
|
|
784
|
-
* deps,
|
|
785
|
-
* 'getUserProfile'
|
|
786
|
-
* )
|
|
787
|
-
* ).scoped(),
|
|
788
|
-
* ```
|
|
789
|
-
*/
|
|
790
|
-
const applyDecorators = (useCase, deps, featureName) => {
|
|
791
|
-
return decorateUseCase(useCase, {
|
|
792
|
-
logger: deps.logger,
|
|
793
|
-
requestId: deps.requestId,
|
|
794
|
-
feature: featureName
|
|
795
|
-
}, featureName);
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
//#endregion
|
|
799
|
-
//#region src/decorators/with-caching.ts
|
|
800
|
-
/**
|
|
801
|
-
* Wraps a use case with caching
|
|
802
|
-
* - Only works with idempotent read operations
|
|
803
|
-
* - Key function determines cache key from input
|
|
804
|
-
* - TTL controls cache expiration
|
|
805
|
-
* - SWR allows returning stale value while revalidating
|
|
806
|
-
*
|
|
807
|
-
* @example
|
|
808
|
-
* ```ts
|
|
809
|
-
* const cachedGetUser = withCaching(
|
|
810
|
-
* getUserProfile,
|
|
811
|
-
* cacheStore,
|
|
812
|
-
* {
|
|
813
|
-
* keyFn: (input) => `user:${input.userId}`,
|
|
814
|
-
* ttlMs: 60000, // 1 minute
|
|
815
|
-
* staleWhileRevalidateMs: 120000, // 2 minutes
|
|
816
|
-
* }
|
|
817
|
-
* )
|
|
818
|
-
* ```
|
|
819
|
-
*/
|
|
820
|
-
const withCaching = (fn, cacheStore, options) => {
|
|
821
|
-
const cached = async (input) => {
|
|
822
|
-
const cacheKey = options.keyFn(input);
|
|
823
|
-
const cached$1 = await cacheStore.get(cacheKey);
|
|
824
|
-
if (cached$1 !== void 0) return cached$1;
|
|
825
|
-
const result = await fn(input);
|
|
826
|
-
await cacheStore.set(cacheKey, result, options.ttlMs);
|
|
827
|
-
return result;
|
|
828
|
-
};
|
|
829
|
-
Object.defineProperty(cached, "name", { value: `withCaching(${fn.name || "anonymous"})` });
|
|
830
|
-
return cached;
|
|
831
|
-
};
|
|
832
|
-
|
|
833
|
-
//#endregion
|
|
834
|
-
//#region src/decorators/with-retry.ts
|
|
835
|
-
/**
|
|
836
|
-
* Wraps a use case with retry logic
|
|
837
|
-
* - Uses exponential backoff
|
|
838
|
-
* - Only retries on specific errors via retryOn predicate
|
|
839
|
-
* - Backoff is capped at maxBackoffMs
|
|
840
|
-
*
|
|
841
|
-
* @example
|
|
842
|
-
* ```ts
|
|
843
|
-
* const retriedGetUser = withRetry(
|
|
844
|
-
* getUserProfile,
|
|
845
|
-
* {
|
|
846
|
-
* maxAttempts: 3,
|
|
847
|
-
* initialBackoffMs: 100,
|
|
848
|
-
* maxBackoffMs: 30000,
|
|
849
|
-
* backoffMultiplier: 2,
|
|
850
|
-
* retryOn: (error) => error.message.includes('TIMEOUT'),
|
|
851
|
-
* }
|
|
852
|
-
* )
|
|
853
|
-
* ```
|
|
854
|
-
*/
|
|
855
|
-
const withRetry = (fn, options = {}) => {
|
|
856
|
-
const maxAttempts = options.maxAttempts ?? 3;
|
|
857
|
-
const initialBackoffMs = options.initialBackoffMs ?? 100;
|
|
858
|
-
const maxBackoffMs = options.maxBackoffMs ?? 3e4;
|
|
859
|
-
const backoffMultiplier = options.backoffMultiplier ?? 2;
|
|
860
|
-
const retryOn = options.retryOn ?? ((error) => error instanceof Error);
|
|
861
|
-
const retried = async (...args) => {
|
|
862
|
-
let lastError = null;
|
|
863
|
-
let backoffMs = initialBackoffMs;
|
|
864
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
|
|
865
|
-
return await fn(...args);
|
|
866
|
-
} catch (error) {
|
|
867
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
868
|
-
if (!retryOn(lastError)) throw error;
|
|
869
|
-
if (attempt < maxAttempts) {
|
|
870
|
-
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
871
|
-
backoffMs = Math.min(backoffMs * backoffMultiplier, maxBackoffMs);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
throw lastError ?? /* @__PURE__ */ new Error("Retry failed without error");
|
|
875
|
-
};
|
|
876
|
-
Object.defineProperty(retried, "name", { value: `withRetry(${fn.name || "anonymous"})` });
|
|
877
|
-
return retried;
|
|
878
|
-
};
|
|
879
|
-
|
|
880
|
-
//#endregion
|
|
881
|
-
//#region src/decorators/with-rate-limit.ts
|
|
882
|
-
/**
|
|
883
|
-
* Rate limit error
|
|
884
|
-
*/
|
|
885
|
-
var RateLimitError = class extends AppError {
|
|
886
|
-
constructor(resetAt) {
|
|
887
|
-
super(ErrorCode.RATE_LIMITED, `Rate limit exceeded. Reset at ${resetAt.toISOString()}`, {
|
|
888
|
-
statusCode: 429,
|
|
889
|
-
meta: { resetAt }
|
|
890
|
-
});
|
|
891
|
-
this.resetAt = resetAt;
|
|
892
|
-
}
|
|
893
|
-
};
|
|
894
|
-
/**
|
|
895
|
-
* Wraps a use case with rate limiting
|
|
896
|
-
* - Uses token bucket algorithm
|
|
897
|
-
* - Per-input rate limit tracking via keyFn
|
|
898
|
-
* - Throws RateLimitError when limit exceeded
|
|
899
|
-
*
|
|
900
|
-
* @example
|
|
901
|
-
* ```ts
|
|
902
|
-
* const limitedCreatePost = withRateLimit(
|
|
903
|
-
* createPost,
|
|
904
|
-
* rateLimiterStore,
|
|
905
|
-
* {
|
|
906
|
-
* keyFn: (input) => `user:${input.userId}`,
|
|
907
|
-
* capacity: 10,
|
|
908
|
-
* refillPerSecond: 1, // 1 request per second
|
|
909
|
-
* }
|
|
910
|
-
* )
|
|
911
|
-
* ```
|
|
912
|
-
*/
|
|
913
|
-
const withRateLimit = (fn, rateLimiterStore, options) => {
|
|
914
|
-
const limited = async (input) => {
|
|
915
|
-
const limitKey = options.keyFn(input);
|
|
916
|
-
const result = await rateLimiterStore.checkLimit(limitKey, options.capacity, options.refillPerSecond);
|
|
917
|
-
if (!result.allowed) throw new RateLimitError(result.resetAt);
|
|
918
|
-
return await fn(input);
|
|
919
|
-
};
|
|
920
|
-
Object.defineProperty(limited, "name", { value: `withRateLimit(${fn.name || "anonymous"})` });
|
|
921
|
-
return limited;
|
|
922
|
-
};
|
|
923
|
-
|
|
924
|
-
//#endregion
|
|
925
|
-
//#region src/modules/core.module.ts
|
|
926
|
-
/**
|
|
927
|
-
* Core module: singleton registrations for infrastructure
|
|
928
|
-
* - Database pool and connection
|
|
929
|
-
* - Clock
|
|
930
|
-
* - Logger (structured, Loki/Prometheus compatible)
|
|
931
|
-
* - AI provider
|
|
932
|
-
* - Auth
|
|
933
|
-
*/
|
|
934
|
-
const registerCoreModule = (container) => {
|
|
935
|
-
container.register({
|
|
936
|
-
dbPool: asFunction(({ config }) => createDbPool(config.databaseUrl)).singleton(),
|
|
937
|
-
db: asFunction(({ dbPool }) => createDb(dbPool)).singleton(),
|
|
938
|
-
clock: asValue(new SystemClock()),
|
|
939
|
-
logger: asFunction(({ config }) => makeStructuredLogger({
|
|
940
|
-
enableFile: config.enableFileLogging,
|
|
941
|
-
logDir: config.logDir,
|
|
942
|
-
minLevel: config.logLevel
|
|
943
|
-
})).singleton(),
|
|
944
|
-
errorHandler: asFunction(({ logger }) => makeErrorHandler(logger)).singleton(),
|
|
945
|
-
auth: asValue(auth),
|
|
946
|
-
aiProvider: asFunction(() => google("gemini-2.5-flash")).singleton(),
|
|
947
|
-
eventBus: asFunction(({ logger }) => new InMemoryEventBus(logger)).singleton(),
|
|
948
|
-
cacheStore: asClass(InMemoryCacheStore).singleton(),
|
|
949
|
-
rateLimiterStore: asClass(InMemoryRateLimiterStore).singleton(),
|
|
950
|
-
requestLogger: asFunction(({ logger, requestId, session }) => makeRequestLogger({
|
|
951
|
-
requestId,
|
|
952
|
-
userId: session?.user?.id,
|
|
953
|
-
baseLogger: logger
|
|
954
|
-
})).scoped()
|
|
955
|
-
});
|
|
956
|
-
};
|
|
957
|
-
|
|
958
|
-
//#endregion
|
|
959
|
-
//#region src/modules/user.module.ts
|
|
960
|
-
/**
|
|
961
|
-
* User repository module: registers user-related repositories
|
|
962
|
-
*
|
|
963
|
-
* Note: Use case registrations are in apps/server/src/modules/user.module.ts
|
|
964
|
-
* to maintain Clean Architecture layering (Infrastructure should not import Application)
|
|
965
|
-
*/
|
|
966
|
-
const registerUserModule = (container) => {
|
|
967
|
-
container.register({ userRepository: asFunction(({ db }) => makeDrizzleUserRepository(db)).scoped() });
|
|
968
|
-
};
|
|
969
|
-
|
|
970
|
-
//#endregion
|
|
971
29
|
export { ErrorHandler, InMemoryCacheStore, InMemoryEventBus, InMemoryRateLimiterStore, RateLimitError, StructuredLogger, applyDecorators, createDb, createDbPool, decorateUseCase, makeDecoratedUseCase, makeDrizzleUserRepository, makeErrorHandler, makeEventPublisherAdapter, makeInMemoryEventPublisher, makeRequestLogger, makeStructuredLogger, registerCoreModule, registerUserModule, runInTransaction, withCaching, withErrorHandling, withPerformanceMonitoring, withRateLimit, withRequestTracing, withRetry };
|