@tumbaland/backend-core 1.16.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/.versionrc.json +7 -0
- package/README.md +179 -0
- package/__mocks__/uuid.js +8 -0
- package/dist/app/createBaseApp.d.ts +44 -0
- package/dist/app/createBaseApp.d.ts.map +1 -0
- package/dist/app/createBaseApp.js +54 -0
- package/dist/app/createBaseApp.js.map +1 -0
- package/dist/config/env.d.ts +8 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +17 -0
- package/dist/config/env.js.map +1 -0
- package/dist/database/connection.d.ts +3 -0
- package/dist/database/connection.d.ts.map +1 -0
- package/dist/database/connection.js +49 -0
- package/dist/database/connection.js.map +1 -0
- package/dist/errors/HttpError.d.ts +32 -0
- package/dist/errors/HttpError.d.ts.map +1 -0
- package/dist/errors/HttpError.js +61 -0
- package/dist/errors/HttpError.js.map +1 -0
- package/dist/health/healthController.d.ts +21 -0
- package/dist/health/healthController.d.ts.map +1 -0
- package/dist/health/healthController.js +56 -0
- package/dist/health/healthController.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.d.ts +4 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +78 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/metrics/index.d.ts +17 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +99 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/middleware/authMiddleware.d.ts +9 -0
- package/dist/middleware/authMiddleware.d.ts.map +1 -0
- package/dist/middleware/authMiddleware.js +32 -0
- package/dist/middleware/authMiddleware.js.map +1 -0
- package/dist/middleware/corsMiddleware.d.ts +20 -0
- package/dist/middleware/corsMiddleware.d.ts.map +1 -0
- package/dist/middleware/corsMiddleware.js +33 -0
- package/dist/middleware/corsMiddleware.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +14 -0
- package/dist/middleware/errorHandler.d.ts.map +1 -0
- package/dist/middleware/errorHandler.js +88 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/requestLogger.d.ts +16 -0
- package/dist/middleware/requestLogger.d.ts.map +1 -0
- package/dist/middleware/requestLogger.js +49 -0
- package/dist/middleware/requestLogger.js.map +1 -0
- package/dist/middleware/security.d.ts +26 -0
- package/dist/middleware/security.d.ts.map +1 -0
- package/dist/middleware/security.js +47 -0
- package/dist/middleware/security.js.map +1 -0
- package/dist/middleware/validate.d.ts +11 -0
- package/dist/middleware/validate.d.ts.map +1 -0
- package/dist/middleware/validate.js +24 -0
- package/dist/middleware/validate.js.map +1 -0
- package/dist/tracing/index.d.ts +13 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +89 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/types/auth.d.ts +32 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +3 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/utils/correlation.d.ts +11 -0
- package/dist/utils/correlation.d.ts.map +1 -0
- package/dist/utils/correlation.js +23 -0
- package/dist/utils/correlation.js.map +1 -0
- package/dist/utils/response.d.ts +24 -0
- package/dist/utils/response.d.ts.map +1 -0
- package/dist/utils/response.js +35 -0
- package/dist/utils/response.js.map +1 -0
- package/jest.config.js +14 -0
- package/package.json +61 -0
- package/src/app/createBaseApp.test.ts +69 -0
- package/src/app/createBaseApp.ts +75 -0
- package/src/config/env.test.ts +28 -0
- package/src/config/env.ts +13 -0
- package/src/database/connection.test.ts +142 -0
- package/src/database/connection.ts +46 -0
- package/src/errors/HttpError.test.ts +38 -0
- package/src/errors/HttpError.ts +57 -0
- package/src/health/healthController.test.ts +91 -0
- package/src/health/healthController.ts +56 -0
- package/src/index.ts +60 -0
- package/src/logging/logger.test.ts +76 -0
- package/src/logging/logger.ts +103 -0
- package/src/metrics/index.test.ts +91 -0
- package/src/metrics/index.ts +110 -0
- package/src/middleware/authMiddleware.test.ts +104 -0
- package/src/middleware/authMiddleware.ts +29 -0
- package/src/middleware/corsMiddleware.test.ts +58 -0
- package/src/middleware/corsMiddleware.ts +36 -0
- package/src/middleware/errorHandler.test.ts +146 -0
- package/src/middleware/errorHandler.ts +98 -0
- package/src/middleware/requestLogger.test.ts +81 -0
- package/src/middleware/requestLogger.ts +47 -0
- package/src/middleware/security.test.ts +45 -0
- package/src/middleware/security.ts +43 -0
- package/src/middleware/validate.test.ts +60 -0
- package/src/middleware/validate.ts +23 -0
- package/src/tracing/index.test.ts +250 -0
- package/src/tracing/index.ts +97 -0
- package/src/types/auth.ts +33 -0
- package/src/utils/correlation.test.ts +47 -0
- package/src/utils/correlation.ts +22 -0
- package/src/utils/response.test.ts +64 -0
- package/src/utils/response.ts +60 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { context, propagation, trace, SpanKind, Tracer } from '@opentelemetry/api';
|
|
2
|
+
import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
|
|
3
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
4
|
+
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
5
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
6
|
+
|
|
7
|
+
// Initialize OpenTelemetry tracer provider, exporting via OTLP/HTTP.
|
|
8
|
+
// Endpoint resolution: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT > JAEGER_ENDPOINT > in-cluster collector default.
|
|
9
|
+
export const initTracer = (serviceName: string): Tracer => {
|
|
10
|
+
const url =
|
|
11
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||
|
|
12
|
+
process.env.JAEGER_ENDPOINT ||
|
|
13
|
+
'http://jaeger-collector:4318/v1/traces';
|
|
14
|
+
|
|
15
|
+
const provider = new NodeTracerProvider({
|
|
16
|
+
resource: resourceFromAttributes({
|
|
17
|
+
[ATTR_SERVICE_NAME]: serviceName,
|
|
18
|
+
[ATTR_SERVICE_VERSION]: process.env.npm_package_version || '1.0.0',
|
|
19
|
+
'deployment.environment': process.env.NODE_ENV || 'development',
|
|
20
|
+
}),
|
|
21
|
+
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({ url }))],
|
|
22
|
+
});
|
|
23
|
+
provider.register();
|
|
24
|
+
|
|
25
|
+
return provider.getTracer(serviceName);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Global tracer instance
|
|
29
|
+
let tracer: Tracer | null = null;
|
|
30
|
+
|
|
31
|
+
export const getTracer = (): Tracer => {
|
|
32
|
+
if (!tracer) {
|
|
33
|
+
tracer = initTracer(process.env.SERVICE_NAME || 'unknown-service');
|
|
34
|
+
}
|
|
35
|
+
return tracer;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Start a new span. `parent` may be a Span or a Context (e.g. from extractSpanContext).
|
|
39
|
+
export const startSpan = (operationName: string, parentSpan?: any) => {
|
|
40
|
+
if (!parentSpan) {
|
|
41
|
+
return getTracer().startSpan(operationName);
|
|
42
|
+
}
|
|
43
|
+
const parentContext =
|
|
44
|
+
typeof parentSpan.spanContext === 'function'
|
|
45
|
+
? trace.setSpan(context.active(), parentSpan)
|
|
46
|
+
: parentSpan;
|
|
47
|
+
return getTracer().startSpan(operationName, undefined, parentContext);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Middleware for Express to create spans for requests
|
|
51
|
+
export const tracingMiddleware = (req: any, res: any, next: any) => {
|
|
52
|
+
const parentContext = propagation.extract(context.active(), req.headers);
|
|
53
|
+
const span = getTracer().startSpan(
|
|
54
|
+
`${req.method} ${req.path}`,
|
|
55
|
+
{ kind: SpanKind.SERVER },
|
|
56
|
+
parentContext
|
|
57
|
+
);
|
|
58
|
+
span.setAttribute('http.method', req.method);
|
|
59
|
+
span.setAttribute('http.url', req.url);
|
|
60
|
+
|
|
61
|
+
// Add span to request for use in handlers
|
|
62
|
+
req.span = span;
|
|
63
|
+
|
|
64
|
+
res.on('finish', () => {
|
|
65
|
+
span.setAttribute('http.status_code', res.statusCode);
|
|
66
|
+
span.end();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
next();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Helper to create child spans
|
|
73
|
+
export const createChildSpan = (operationName: string, parentSpan: any) => {
|
|
74
|
+
return startSpan(operationName, parentSpan);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Helper to log events to spans
|
|
78
|
+
export const logToSpan = (span: any, event: string, data?: any) => {
|
|
79
|
+
span.addEvent(event, data);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Helper to set tags on spans
|
|
83
|
+
export const setSpanTag = (span: any, key: string, value: any) => {
|
|
84
|
+
span.setAttribute(key, value);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Helper to inject tracing headers (W3C traceparent) for downstream calls
|
|
88
|
+
export const injectHeaders = (span: any) => {
|
|
89
|
+
const headers: { [key: string]: string } = {};
|
|
90
|
+
propagation.inject(trace.setSpan(context.active(), span), headers);
|
|
91
|
+
return headers;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Helper to extract span context from incoming headers; pass the result to startSpan as parent
|
|
95
|
+
export const extractSpanContext = (headers: any) => {
|
|
96
|
+
return propagation.extract(context.active(), headers);
|
|
97
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface UserPayload {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
name: string;
|
|
5
|
+
picture?: string;
|
|
6
|
+
groups?: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Attaches `user`/`userGroups` to every Express `Request` across all
|
|
11
|
+
* services, replacing the ad-hoc `(req as any).user` casts and each
|
|
12
|
+
* service's own divergent local augmentation. Individual services may still
|
|
13
|
+
* carry additional request-scoped fields (e.g. auth-service's `dbUser`) —
|
|
14
|
+
* those stay local since they aren't meaningful outside that service.
|
|
15
|
+
*/
|
|
16
|
+
declare global {
|
|
17
|
+
namespace Express {
|
|
18
|
+
interface Request {
|
|
19
|
+
user?: UserPayload;
|
|
20
|
+
userGroups?: string[];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ServiceHealth {
|
|
26
|
+
status: 'ok' | 'error';
|
|
27
|
+
service: string;
|
|
28
|
+
type: 'backend' | 'frontend';
|
|
29
|
+
timestamp: string;
|
|
30
|
+
version: string;
|
|
31
|
+
description: string;
|
|
32
|
+
dependencies?: Record<string, any>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import { generateCorrelationId, correlationMiddleware } from './correlation';
|
|
3
|
+
|
|
4
|
+
function mockReq(headers: Record<string, string> = {}): Request {
|
|
5
|
+
return { headers } as unknown as Request;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function mockRes(): Response {
|
|
9
|
+
const res: Partial<Response> = {};
|
|
10
|
+
res.setHeader = jest.fn().mockReturnValue(res);
|
|
11
|
+
return res as Response;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('generateCorrelationId', () => {
|
|
15
|
+
it('returns a unique UUID each call', () => {
|
|
16
|
+
const a = generateCorrelationId();
|
|
17
|
+
const b = generateCorrelationId();
|
|
18
|
+
expect(a).not.toBe(b);
|
|
19
|
+
expect(a).toMatch(/^[0-9a-f-]{36}$/i);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('correlationMiddleware', () => {
|
|
24
|
+
it('generates a new correlation id when none is provided', () => {
|
|
25
|
+
const req = mockReq();
|
|
26
|
+
const res = mockRes();
|
|
27
|
+
const next = jest.fn();
|
|
28
|
+
|
|
29
|
+
correlationMiddleware(req, res, next);
|
|
30
|
+
|
|
31
|
+
expect((req as any).correlationId).toMatch(/^[0-9a-f-]{36}$/i);
|
|
32
|
+
expect(res.setHeader).toHaveBeenCalledWith('x-correlation-id', (req as any).correlationId);
|
|
33
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('reuses an incoming x-correlation-id header', () => {
|
|
37
|
+
const req = mockReq({ 'x-correlation-id': 'existing-id' });
|
|
38
|
+
const res = mockRes();
|
|
39
|
+
const next = jest.fn();
|
|
40
|
+
|
|
41
|
+
correlationMiddleware(req, res, next);
|
|
42
|
+
|
|
43
|
+
expect((req as any).correlationId).toBe('existing-id');
|
|
44
|
+
expect(res.setHeader).toHaveBeenCalledWith('x-correlation-id', 'existing-id');
|
|
45
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a unique correlation ID for request tracing
|
|
6
|
+
*/
|
|
7
|
+
export function generateCorrelationId(): string {
|
|
8
|
+
return uuidv4();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Middleware to add correlation ID to requests
|
|
13
|
+
* Adds correlation ID to request object and response headers
|
|
14
|
+
*/
|
|
15
|
+
export const correlationMiddleware: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
|
|
16
|
+
const correlationId = req.headers['x-correlation-id'] as string || generateCorrelationId();
|
|
17
|
+
|
|
18
|
+
(req as any).correlationId = correlationId;
|
|
19
|
+
res.setHeader('x-correlation-id', correlationId);
|
|
20
|
+
|
|
21
|
+
next();
|
|
22
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Response } from 'express';
|
|
2
|
+
import { createApiResponse, sendSuccess, sendError } from './response';
|
|
3
|
+
|
|
4
|
+
function mockRes(): Response {
|
|
5
|
+
const res: Partial<Response> = {};
|
|
6
|
+
res.status = jest.fn().mockReturnValue(res);
|
|
7
|
+
res.json = jest.fn().mockReturnValue(res);
|
|
8
|
+
return res as Response;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('createApiResponse', () => {
|
|
12
|
+
it('omits undefined optional fields', () => {
|
|
13
|
+
expect(createApiResponse(true)).toEqual({ success: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('includes provided fields', () => {
|
|
17
|
+
expect(createApiResponse(true, { id: 1 }, 'ok', undefined, 'corr-1')).toEqual({
|
|
18
|
+
success: true,
|
|
19
|
+
data: { id: 1 },
|
|
20
|
+
message: 'ok',
|
|
21
|
+
correlationId: 'corr-1'
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('keeps data even when falsy but defined (e.g. 0 or empty array)', () => {
|
|
26
|
+
expect(createApiResponse(true, 0)).toEqual({ success: true, data: 0 });
|
|
27
|
+
expect(createApiResponse(true, [])).toEqual({ success: true, data: [] });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('sendSuccess', () => {
|
|
32
|
+
it('defaults to status 200 with a success envelope', () => {
|
|
33
|
+
const res = mockRes();
|
|
34
|
+
sendSuccess(res, { id: 1 }, 'created');
|
|
35
|
+
|
|
36
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
37
|
+
expect(res.json).toHaveBeenCalledWith({ success: true, data: { id: 1 }, message: 'created' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('honors a custom status code', () => {
|
|
41
|
+
const res = mockRes();
|
|
42
|
+
sendSuccess(res, { id: 1 }, 'created', 201);
|
|
43
|
+
|
|
44
|
+
expect(res.status).toHaveBeenCalledWith(201);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('sendError', () => {
|
|
49
|
+
it('defaults to status 500 with an error envelope', () => {
|
|
50
|
+
const res = mockRes();
|
|
51
|
+
sendError(res, 'Something broke');
|
|
52
|
+
|
|
53
|
+
expect(res.status).toHaveBeenCalledWith(500);
|
|
54
|
+
expect(res.json).toHaveBeenCalledWith({ success: false, message: 'Something broke' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('honors a custom status code and error code', () => {
|
|
58
|
+
const res = mockRes();
|
|
59
|
+
sendError(res, 'Not found', 404, 'NOT_FOUND');
|
|
60
|
+
|
|
61
|
+
expect(res.status).toHaveBeenCalledWith(404);
|
|
62
|
+
expect(res.json).toHaveBeenCalledWith({ success: false, message: 'Not found', error: 'NOT_FOUND' });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Response } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standard API response format
|
|
5
|
+
*/
|
|
6
|
+
export interface ApiResponse<T = any> {
|
|
7
|
+
success: boolean;
|
|
8
|
+
data?: T;
|
|
9
|
+
message?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
correlationId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a standardized API response
|
|
16
|
+
*/
|
|
17
|
+
export function createApiResponse<T = any>(
|
|
18
|
+
success: boolean,
|
|
19
|
+
data?: T,
|
|
20
|
+
message?: string,
|
|
21
|
+
error?: string,
|
|
22
|
+
correlationId?: string
|
|
23
|
+
): ApiResponse<T> {
|
|
24
|
+
const response: ApiResponse<T> = { success };
|
|
25
|
+
|
|
26
|
+
if (data !== undefined) response.data = data;
|
|
27
|
+
if (message) response.message = message;
|
|
28
|
+
if (error) response.error = error;
|
|
29
|
+
if (correlationId) response.correlationId = correlationId;
|
|
30
|
+
|
|
31
|
+
return response;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Send a success response
|
|
36
|
+
*/
|
|
37
|
+
export function sendSuccess<T = any>(
|
|
38
|
+
res: Response,
|
|
39
|
+
data?: T,
|
|
40
|
+
message?: string,
|
|
41
|
+
statusCode: number = 200,
|
|
42
|
+
correlationId?: string
|
|
43
|
+
) {
|
|
44
|
+
const response = createApiResponse(true, data, message, undefined, correlationId);
|
|
45
|
+
res.status(statusCode).json(response);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Send an error response
|
|
50
|
+
*/
|
|
51
|
+
export function sendError(
|
|
52
|
+
res: Response,
|
|
53
|
+
message: string,
|
|
54
|
+
statusCode: number = 500,
|
|
55
|
+
error?: string,
|
|
56
|
+
correlationId?: string
|
|
57
|
+
) {
|
|
58
|
+
const response = createApiResponse(false, undefined, message, error, correlationId);
|
|
59
|
+
res.status(statusCode).json(response);
|
|
60
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2023",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"types": ["jest", "node"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|