@zaaxch/tailframe 4.0.4 → 4.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/new.mjs +6 -1
- package/src/owned-sources.mjs +47 -1
- package/src/service-templates.mjs +135 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zaaxch/tailframe",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.5",
|
|
4
4
|
"description": "Tailframe architecture toolkit: validates the Tailframe structure, import-boundary, and file-convention contracts. The package version is the contract version.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/new.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
healthRoutesSource,
|
|
19
19
|
healthSchemasSource,
|
|
20
20
|
httpErrorsSource,
|
|
21
|
+
observabilitySource,
|
|
21
22
|
mongoReadinessProbeSource,
|
|
22
23
|
postgresReadinessProbeSource,
|
|
23
24
|
readinessProbeSource,
|
|
@@ -221,6 +222,7 @@ const svcDeps = {
|
|
|
221
222
|
joi: "^17.13.3",
|
|
222
223
|
"reflect-metadata": "^0.2.2",
|
|
223
224
|
tsyringe: "^4.9.1",
|
|
225
|
+
winston: "^3.19.0",
|
|
224
226
|
...(postgres ? { pg: "^8.16.3" } : { mongodb: "^6.17.0" }),
|
|
225
227
|
...(options.auth === "firebase" ? { "firebase-admin": "^13.0.0" } : {}),
|
|
226
228
|
...(options.redis ? { "rate-limiter-flexible": "^11.2.0", redis: "^5.10.0" } : {})
|
|
@@ -331,6 +333,7 @@ if (options.redis) add(`${svc}/src/platform/redis/index.ts`, redisSource);
|
|
|
331
333
|
add(`${svc}/src/core/RequestContext.ts`, requestContextSource);
|
|
332
334
|
add(`${svc}/src/core/UseCase.ts`, useCaseSource);
|
|
333
335
|
add(`${svc}/src/core/errors.ts`, appErrorSource);
|
|
336
|
+
add(`${svc}/src/app/observability.ts`, observabilitySource);
|
|
334
337
|
add(`${svc}/src/platform/http/createRequestContext.ts`, createRequestContextSource(options.auth));
|
|
335
338
|
if (options.auth === "firebase") add(`${svc}/src/platform/auth/firebase.ts`, firebaseSource);
|
|
336
339
|
add(`${svc}/src/platform/http/rpc.ts`, rpcSource);
|
|
@@ -480,7 +483,7 @@ These conventions apply to \`${svc}\`. This service is a modular monolith organi
|
|
|
480
483
|
## Architecture vocabulary
|
|
481
484
|
- Use \`src/app\` for process assembly, server creation, route mounting, and dependency registration.
|
|
482
485
|
- Use \`src/core\` only for small technology-neutral contracts such as \`RequestContext\` and \`UseCase\`.
|
|
483
|
-
- Use \`src/platform\` for technical mechanisms such as Express, authentication, configuration, databases, Redis, and shared HTTP behavior.
|
|
486
|
+
- Use \`src/platform\` for technical mechanisms such as Express, authentication, configuration, databases, Redis, structured logging, and shared HTTP behavior.
|
|
484
487
|
- Use \`src/modules/<module>\` for product capabilities.
|
|
485
488
|
- These are the only service architecture roots. Keep \`src/core\` flat. Put external-system adapters under \`src/platform/integrations/<provider>\`; directories immediately under \`src/app\` are limited to \`cli\`, \`jobs\`, \`mcp\`, \`scheduled-tasks\`, \`workers\`, and \`__tests__\`.
|
|
486
489
|
- Inside a module, use \`use-cases/\`, \`http/\`, and tests first. Add \`domain/\` or \`persistence/\` only when behavior requires them.
|
|
@@ -496,6 +499,7 @@ These conventions apply to \`${svc}\`. This service is a modular monolith organi
|
|
|
496
499
|
| Express route or request schema | Owning module's \`http\` |
|
|
497
500
|
| Vendor or external-system adapter | \`src/platform/integrations/<provider>\` |
|
|
498
501
|
| Database, Redis, authentication, or HTTP mechanism | \`src/platform\` |
|
|
502
|
+
| Product-specific Winston transport | \`src/app/observability.ts\` |
|
|
499
503
|
| Process lifecycle, registration, schedule, or consumer | \`src/app/<entry-point-kind>\` |
|
|
500
504
|
| Cross-entry-point technology-neutral contract | Flat \`src/core\` |
|
|
501
505
|
| Root executable | Bootstrap of matching \`src/app\` assembly only |
|
|
@@ -544,6 +548,7 @@ ${postgres
|
|
|
544
548
|
- Keep optional entry-point assembly under \`src/app/<entry-point-kind>\`; root files such as \`src/server.ts\` and \`src/worker.ts\` may only bootstrap that assembly.
|
|
545
549
|
- Initialize infrastructure first, then call \`registerDependencies(...)\` from every process entry point. The composition root resets the container, explicitly constructs dependencies, and registers class-token instances. Keep tsyringe, injection tokens, decorators, and container resolution out of modules and platform adapters.
|
|
546
550
|
- Register routes, workers, and jobs explicitly. Shutdown paths are awaitable and close resources without forcing \`process.exit\`.
|
|
551
|
+
- The canonical Winston JSON console transport logs every HTTP failure with safe request metadata. Add product-specific transports only in \`src/app/observability.ts\`; do not remove, silence, or reconfigure the canonical transport. Preserve diagnostic causes on translated errors without exposing them in client envelopes.
|
|
547
552
|
- Scheduled work prevents unintended overlap, stops intake before shutdown, awaits the active run, destroys its scheduler or consumer, and only then closes Redis and database clients.
|
|
548
553
|
${options.ui ? "- In production, serve the compiled UI from `dist/public` with a non-API SPA fallback. Never return the SPA for `/api` requests." : ""}
|
|
549
554
|
|
package/src/owned-sources.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
csrfSource,
|
|
6
6
|
firebaseSource,
|
|
7
7
|
httpErrorsSource,
|
|
8
|
+
loggerSource,
|
|
8
9
|
mongoUsersSource,
|
|
9
10
|
rateLimitSource,
|
|
10
11
|
readEnvSource,
|
|
@@ -77,6 +78,11 @@ it("maps verified and unverified Firebase email claims and verifies only once",
|
|
|
77
78
|
|
|
78
79
|
const errorTranslationTestSource = `import { AppError, type FailureKind } from "@/core/errors";
|
|
79
80
|
import { errorHandler } from "@/platform/http/errors";
|
|
81
|
+
import { logger } from "@/platform/logging/logger";
|
|
82
|
+
|
|
83
|
+
jest.mock("@/platform/logging/logger", () => ({ logger: { log: jest.fn() }, serializeError: (error: unknown) => error }));
|
|
84
|
+
|
|
85
|
+
const log = logger.log as jest.MockedFunction<typeof logger.log>;
|
|
80
86
|
|
|
81
87
|
const statuses: Array<[FailureKind, number]> = [
|
|
82
88
|
["invalid", 400],
|
|
@@ -91,11 +97,50 @@ const statuses: Array<[FailureKind, number]> = [
|
|
|
91
97
|
];
|
|
92
98
|
|
|
93
99
|
it.each(statuses)("translates %s to HTTP %i", (kind, status) => {
|
|
100
|
+
log.mockClear();
|
|
94
101
|
const json = jest.fn();
|
|
95
102
|
const response = { status: jest.fn(() => ({ json })) } as never;
|
|
96
|
-
|
|
103
|
+
const request = { method: "POST", path: "/health.get", requestContext: { requestId: "request-1" } } as never;
|
|
104
|
+
errorHandler(new AppError({ code: "CODE", message: "message", kind }), request, response, jest.fn());
|
|
97
105
|
expect((response as { status: jest.Mock }).status).toHaveBeenCalledWith(status);
|
|
98
106
|
expect(json).toHaveBeenCalledWith({ error: { code: "CODE", message: "message" } });
|
|
107
|
+
expect(log).toHaveBeenCalledWith(expect.objectContaining({
|
|
108
|
+
level: status >= 500 ? "error" : "warn",
|
|
109
|
+
event: "http.request.failed",
|
|
110
|
+
requestId: "request-1",
|
|
111
|
+
code: "CODE",
|
|
112
|
+
kind
|
|
113
|
+
}));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("logs an unknown failure without exposing it to the client", () => {
|
|
117
|
+
log.mockClear();
|
|
118
|
+
const json = jest.fn();
|
|
119
|
+
const response = { status: jest.fn(() => ({ json })) } as never;
|
|
120
|
+
const error = new Error("database disconnected");
|
|
121
|
+
errorHandler(error, { method: "POST", path: "/health.get" } as never, response, jest.fn());
|
|
122
|
+
expect(json).toHaveBeenCalledWith({ error: { code: "INTERNAL", message: "Internal server error" } });
|
|
123
|
+
expect(log).toHaveBeenCalledWith(expect.objectContaining({
|
|
124
|
+
level: "error",
|
|
125
|
+
code: "INTERNAL",
|
|
126
|
+
error: expect.objectContaining({ message: "database disconnected" })
|
|
127
|
+
}));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("logs a preserved application-error cause", () => {
|
|
131
|
+
log.mockClear();
|
|
132
|
+
const json = jest.fn();
|
|
133
|
+
const response = { status: jest.fn(() => ({ json })) } as never;
|
|
134
|
+
const cause = new Error("Firebase token verification failed");
|
|
135
|
+
errorHandler(
|
|
136
|
+
AppError.unauthenticated(cause),
|
|
137
|
+
{ method: "POST", path: "/health.get" } as never,
|
|
138
|
+
response,
|
|
139
|
+
jest.fn()
|
|
140
|
+
);
|
|
141
|
+
expect(log).toHaveBeenCalledWith(expect.objectContaining({
|
|
142
|
+
error: expect.objectContaining({ cause: expect.objectContaining({ message: "Firebase token verification failed" }) })
|
|
143
|
+
}));
|
|
99
144
|
});
|
|
100
145
|
`;
|
|
101
146
|
|
|
@@ -188,6 +233,7 @@ export function ownedSources(config) {
|
|
|
188
233
|
"src/core/errors.ts": appErrorSource,
|
|
189
234
|
"src/core/SchemaLifecycle.ts": schemaLifecycleSource,
|
|
190
235
|
"src/platform/config/readEnv.ts": readEnvSource,
|
|
236
|
+
"src/platform/logging/logger.ts": loggerSource,
|
|
191
237
|
"src/platform/http/createRequestContext.ts": createRequestContextSource(profiles.has("firebase") ? "firebase" : "none"),
|
|
192
238
|
"src/platform/http/csrf.ts": csrfSource,
|
|
193
239
|
"src/platform/http/errors.ts": httpErrorsSource,
|
|
@@ -43,20 +43,61 @@ export const appErrorSource = `export type FailureKind =
|
|
|
43
43
|
| "unavailable"
|
|
44
44
|
| "internal";
|
|
45
45
|
|
|
46
|
+
export interface AppErrorOptions {
|
|
47
|
+
code: string;
|
|
48
|
+
message: string;
|
|
49
|
+
kind: FailureKind;
|
|
50
|
+
details?: unknown;
|
|
51
|
+
cause?: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
export class AppError extends Error {
|
|
47
55
|
readonly name = "AppError";
|
|
56
|
+
readonly code: string;
|
|
57
|
+
readonly kind: FailureKind;
|
|
58
|
+
readonly details: unknown;
|
|
59
|
+
|
|
60
|
+
constructor({ code, message, kind, details, cause }: AppErrorOptions) {
|
|
61
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
62
|
+
this.code = code;
|
|
63
|
+
this.kind = kind;
|
|
64
|
+
this.details = details;
|
|
65
|
+
}
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
message: string,
|
|
52
|
-
readonly kind: FailureKind,
|
|
53
|
-
readonly details?: unknown
|
|
54
|
-
) {
|
|
55
|
-
super(message);
|
|
67
|
+
static unauthenticated(cause?: unknown): AppError {
|
|
68
|
+
return new AppError({ code: "UNAUTHENTICATED", message: "Unauthorized", kind: "unauthenticated", cause });
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
`;
|
|
59
72
|
|
|
73
|
+
export const loggerSource = `import { createLogger, format, transports } from "winston";
|
|
74
|
+
|
|
75
|
+
export function serializeError(error: unknown, depth = 0): unknown {
|
|
76
|
+
if (!(error instanceof Error)) return error;
|
|
77
|
+
return {
|
|
78
|
+
name: error.name,
|
|
79
|
+
message: error.message,
|
|
80
|
+
stack: error.stack,
|
|
81
|
+
...(error.cause === undefined || depth >= 3 ? {} : { cause: serializeError(error.cause, depth + 1) })
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const serializeErrorMetadata = format((info) => {
|
|
86
|
+
if (info.error !== undefined) info.error = serializeError(info.error);
|
|
87
|
+
return info;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The service-wide logger. Tailframe always retains the JSON console transport so
|
|
92
|
+
* canonical failure logging cannot be disabled by application assembly.
|
|
93
|
+
*/
|
|
94
|
+
export const logger = createLogger({
|
|
95
|
+
level: "info",
|
|
96
|
+
format: format.combine(format.timestamp(), format.errors({ stack: true }), serializeErrorMetadata(), format.json()),
|
|
97
|
+
transports: [new transports.Console()]
|
|
98
|
+
});
|
|
99
|
+
`;
|
|
100
|
+
|
|
60
101
|
export function createRequestContextSource(auth) {
|
|
61
102
|
if (auth === "firebase") {
|
|
62
103
|
return `import { randomUUID } from "node:crypto";
|
|
@@ -88,7 +129,7 @@ export async function createRequestContext(req: Request): Promise<RequestContext
|
|
|
88
129
|
return context;
|
|
89
130
|
}
|
|
90
131
|
if (!/^Bearer [^\\s]+$/.test(header)) {
|
|
91
|
-
throw
|
|
132
|
+
throw AppError.unauthenticated();
|
|
92
133
|
}
|
|
93
134
|
|
|
94
135
|
try {
|
|
@@ -99,8 +140,8 @@ export async function createRequestContext(req: Request): Promise<RequestContext
|
|
|
99
140
|
};
|
|
100
141
|
req.requestContext = context;
|
|
101
142
|
return context;
|
|
102
|
-
} catch {
|
|
103
|
-
throw
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw AppError.unauthenticated(error);
|
|
104
145
|
}
|
|
105
146
|
}
|
|
106
147
|
|
|
@@ -200,9 +241,10 @@ export function rpcHandler<Input, Output, WireInput = Input>(
|
|
|
200
241
|
}
|
|
201
242
|
`;
|
|
202
243
|
|
|
203
|
-
export const httpErrorsSource = `import type { ErrorRequestHandler } from "express";
|
|
244
|
+
export const httpErrorsSource = `import type { ErrorRequestHandler, Request } from "express";
|
|
204
245
|
import Joi from "joi";
|
|
205
246
|
import { AppError } from "@/core/errors";
|
|
247
|
+
import { logger, serializeError } from "@/platform/logging/logger";
|
|
206
248
|
|
|
207
249
|
const statusByKind: Readonly<Record<AppError["kind"], number>> = {
|
|
208
250
|
invalid: 400,
|
|
@@ -216,9 +258,26 @@ const statusByKind: Readonly<Record<AppError["kind"], number>> = {
|
|
|
216
258
|
internal: 500
|
|
217
259
|
};
|
|
218
260
|
|
|
219
|
-
|
|
261
|
+
function logFailure(req: Request, error: unknown, status: number, code: string, kind?: AppError["kind"]) {
|
|
262
|
+
logger.log({
|
|
263
|
+
level: status >= 500 ? "error" : "warn",
|
|
264
|
+
message: "HTTP request failed",
|
|
265
|
+
event: "http.request.failed",
|
|
266
|
+
requestId: req.requestContext?.requestId,
|
|
267
|
+
method: req.method,
|
|
268
|
+
path: req.path,
|
|
269
|
+
status,
|
|
270
|
+
code,
|
|
271
|
+
...(kind === undefined ? {} : { kind }),
|
|
272
|
+
error: serializeError(error)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export const errorHandler: ErrorRequestHandler = (error, req, res, _next) => {
|
|
220
277
|
if (error instanceof AppError) {
|
|
221
|
-
|
|
278
|
+
const status = statusByKind[error.kind];
|
|
279
|
+
logFailure(req, error, status, error.code, error.kind);
|
|
280
|
+
res.status(status).json({
|
|
222
281
|
error: {
|
|
223
282
|
code: error.code,
|
|
224
283
|
message: error.message,
|
|
@@ -228,6 +287,7 @@ export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
|
|
|
228
287
|
return;
|
|
229
288
|
}
|
|
230
289
|
if (Joi.isError(error)) {
|
|
290
|
+
logFailure(req, error, 400, "VALIDATION_FAILED", "invalid");
|
|
231
291
|
res.status(400).json({
|
|
232
292
|
error: {
|
|
233
293
|
code: "VALIDATION_FAILED",
|
|
@@ -237,11 +297,22 @@ export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
|
|
|
237
297
|
});
|
|
238
298
|
return;
|
|
239
299
|
}
|
|
240
|
-
|
|
300
|
+
logFailure(req, error, 500, "INTERNAL", "internal");
|
|
241
301
|
res.status(500).json({ error: { code: "INTERNAL", message: "Internal server error" } });
|
|
242
302
|
};
|
|
243
303
|
`;
|
|
244
304
|
|
|
305
|
+
export const observabilitySource = `import type { Logger } from "winston";
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Add product-specific Winston transports here. Do not remove or reconfigure the
|
|
309
|
+
* Tailframe console transport: it is the required baseline for canonical errors.
|
|
310
|
+
*/
|
|
311
|
+
export function configureObservability(_logger: Pick<Logger, "add">): void {
|
|
312
|
+
// Add product-specific transports and leave the canonical console transport intact.
|
|
313
|
+
}
|
|
314
|
+
`;
|
|
315
|
+
|
|
245
316
|
export const csrfSource = `import type { RequestHandler } from "express";
|
|
246
317
|
import { AppError } from "@/core/errors";
|
|
247
318
|
|
|
@@ -255,7 +326,7 @@ export function csrfHeaderGuard(exemptPaths: readonly string[] = []): RequestHan
|
|
|
255
326
|
!exempt.has(req.path) &&
|
|
256
327
|
req.headers["x-requested-with"] !== "XMLHttpRequest"
|
|
257
328
|
) {
|
|
258
|
-
next(new AppError("CSRF_HEADER_MISSING", "CSRF header missing", "forbidden"));
|
|
329
|
+
next(new AppError({ code: "CSRF_HEADER_MISSING", message: "CSRF header missing", kind: "forbidden" }));
|
|
259
330
|
return;
|
|
260
331
|
}
|
|
261
332
|
next();
|
|
@@ -271,6 +342,7 @@ export const schemaLifecycleSource = `export interface SchemaLifecycle {
|
|
|
271
342
|
|
|
272
343
|
export const applySchemaCliSource = `import "reflect-metadata";
|
|
273
344
|
import { schemaLifecycle } from "@/app/schema";
|
|
345
|
+
import { logger } from "@/platform/logging/logger";
|
|
274
346
|
|
|
275
347
|
async function main() {
|
|
276
348
|
try {
|
|
@@ -281,7 +353,7 @@ async function main() {
|
|
|
281
353
|
}
|
|
282
354
|
|
|
283
355
|
void main().catch((error) => {
|
|
284
|
-
|
|
356
|
+
logger.error("Schema application failed", { event: "schema.apply.failed", error });
|
|
285
357
|
process.exitCode = 1;
|
|
286
358
|
});
|
|
287
359
|
`;
|
|
@@ -343,9 +415,9 @@ export function createRateLimiter(client: RedisClientType, policy: RateLimitPoli
|
|
|
343
415
|
next();
|
|
344
416
|
} catch (error) {
|
|
345
417
|
if (error instanceof RateLimiterRes)
|
|
346
|
-
next(new AppError("RATE_LIMITED", "Too many requests", "rate_limited"));
|
|
418
|
+
next(new AppError({ code: "RATE_LIMITED", message: "Too many requests", kind: "rate_limited" }));
|
|
347
419
|
else if (error instanceof AppError) next(error);
|
|
348
|
-
else next(new AppError("RATE_LIMIT_UNAVAILABLE", "Request protection unavailable", "unavailable"));
|
|
420
|
+
else next(new AppError({ code: "RATE_LIMIT_UNAVAILABLE", message: "Request protection unavailable", kind: "unavailable", cause: error }));
|
|
349
421
|
}
|
|
350
422
|
};
|
|
351
423
|
}
|
|
@@ -426,8 +498,8 @@ export class GetReadiness implements UseCase<Record<string, never>, ReadinessRes
|
|
|
426
498
|
try {
|
|
427
499
|
await Promise.all(this.probes.map((probe) => probe.check()));
|
|
428
500
|
return { status: "ready" };
|
|
429
|
-
} catch {
|
|
430
|
-
throw new AppError("NOT_READY", "Service is not ready", "unavailable");
|
|
501
|
+
} catch (error) {
|
|
502
|
+
throw new AppError({ code: "NOT_READY", message: "Service is not ready", kind: "unavailable", cause: error });
|
|
431
503
|
}
|
|
432
504
|
}
|
|
433
505
|
}
|
|
@@ -496,9 +568,10 @@ export class RedisReadinessProbe implements ReadinessProbe {
|
|
|
496
568
|
|
|
497
569
|
export const redisSource = `import { createClient, type RedisClientType } from "redis";
|
|
498
570
|
import { env } from "@/platform/config/env";
|
|
571
|
+
import { logger } from "@/platform/logging/logger";
|
|
499
572
|
|
|
500
573
|
export const redis: RedisClientType = createClient({ url: env.redisUrl });
|
|
501
|
-
redis.on("error", (error) =>
|
|
574
|
+
redis.on("error", (error) => logger.error("Redis client error", { event: "redis.client.error", error }));
|
|
502
575
|
|
|
503
576
|
export async function connectRedis() {
|
|
504
577
|
if (!redis.isOpen) await redis.connect();
|
|
@@ -563,10 +636,12 @@ export function serverSource({ ui, redis, csrf }) {
|
|
|
563
636
|
return `${ui ? 'import path from "node:path";\n' : ""}import express from "express";
|
|
564
637
|
import cors from "cors";
|
|
565
638
|
import { registerDependencies } from "@/app/container";
|
|
639
|
+
import { configureObservability } from "@/app/observability";
|
|
566
640
|
import { applicationRoutes } from "@/app/routes";
|
|
567
641
|
import { env } from "@/platform/config/env";
|
|
568
642
|
import { closeDatabase, connectDatabase } from "@/platform/database";
|
|
569
643
|
${redis ? 'import { closeRedis, connectRedis } from "@/platform/redis";\n' : ""}${csrf ? 'import { csrfHeaderGuard } from "@/platform/http/csrf";\n' : ""}import { errorHandler } from "@/platform/http/errors";
|
|
644
|
+
import { logger } from "@/platform/logging/logger";
|
|
570
645
|
|
|
571
646
|
export interface ApplicationOptions {
|
|
572
647
|
${ui ? "\tpublicPath?: string;\n\tproduction?: boolean;\n" : ""}}
|
|
@@ -582,43 +657,57 @@ ${ui ? '\tif (production) {\n\t\tapp.get(/^(?!\\/api(?:\\/|$)).*/, (_req, res) =
|
|
|
582
657
|
}
|
|
583
658
|
|
|
584
659
|
export async function startServer() {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
660
|
+
try {
|
|
661
|
+
configureObservability(logger);
|
|
662
|
+
const db = await connectDatabase();
|
|
663
|
+
${redis ? "\t\tconst redis = await connectRedis();\n" : ""} registerDependencies({ db${redis ? ", redis" : ""} });
|
|
664
|
+
const app = createApplication();
|
|
665
|
+
const server = app.listen(env.port);
|
|
666
|
+
const shutdown = async () => {
|
|
667
|
+
await new Promise<void>((resolve, reject) => {
|
|
668
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
669
|
+
});
|
|
670
|
+
${redis ? "\t\t\tawait closeRedis();\n" : ""} await closeDatabase();
|
|
671
|
+
};
|
|
672
|
+
process.once("SIGINT", () => void shutdown());
|
|
673
|
+
process.once("SIGTERM", () => void shutdown());
|
|
674
|
+
return { app, server, shutdown };
|
|
675
|
+
} catch (error) {
|
|
676
|
+
logger.error("Server startup failed", { event: "server.start.failed", error });
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
598
679
|
}
|
|
599
680
|
`;
|
|
600
681
|
}
|
|
601
682
|
|
|
602
683
|
export function workerSource() {
|
|
603
684
|
return `import { container, registerDependencies } from "@/app/container";
|
|
685
|
+
import { configureObservability } from "@/app/observability";
|
|
604
686
|
import { GetReadiness } from "@/modules/health/use-cases/GetReadiness";
|
|
605
687
|
import { closeDatabase, connectDatabase } from "@/platform/database";
|
|
606
688
|
import { systemRequestContext } from "@/platform/http/createRequestContext";
|
|
689
|
+
import { logger } from "@/platform/logging/logger";
|
|
607
690
|
import { closeRedis, connectRedis } from "@/platform/redis";
|
|
608
691
|
|
|
609
692
|
export async function startWorker() {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
693
|
+
try {
|
|
694
|
+
configureObservability(logger);
|
|
695
|
+
const db = await connectDatabase();
|
|
696
|
+
const redis = await connectRedis();
|
|
697
|
+
registerDependencies({ db, redis });
|
|
698
|
+
await container.resolve(GetReadiness).execute(systemRequestContext(), {});
|
|
699
|
+
|
|
700
|
+
const shutdown = async () => {
|
|
701
|
+
await closeRedis();
|
|
702
|
+
await closeDatabase();
|
|
703
|
+
};
|
|
704
|
+
process.once("SIGINT", () => void shutdown());
|
|
705
|
+
process.once("SIGTERM", () => void shutdown());
|
|
706
|
+
return { shutdown };
|
|
707
|
+
} catch (error) {
|
|
708
|
+
logger.error("Worker startup failed", { event: "worker.start.failed", error });
|
|
709
|
+
throw error;
|
|
710
|
+
}
|
|
622
711
|
}
|
|
623
712
|
`;
|
|
624
713
|
}
|