@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.
Files changed (113) hide show
  1. package/.versionrc.json +7 -0
  2. package/README.md +179 -0
  3. package/__mocks__/uuid.js +8 -0
  4. package/dist/app/createBaseApp.d.ts +44 -0
  5. package/dist/app/createBaseApp.d.ts.map +1 -0
  6. package/dist/app/createBaseApp.js +54 -0
  7. package/dist/app/createBaseApp.js.map +1 -0
  8. package/dist/config/env.d.ts +8 -0
  9. package/dist/config/env.d.ts.map +1 -0
  10. package/dist/config/env.js +17 -0
  11. package/dist/config/env.js.map +1 -0
  12. package/dist/database/connection.d.ts +3 -0
  13. package/dist/database/connection.d.ts.map +1 -0
  14. package/dist/database/connection.js +49 -0
  15. package/dist/database/connection.js.map +1 -0
  16. package/dist/errors/HttpError.d.ts +32 -0
  17. package/dist/errors/HttpError.d.ts.map +1 -0
  18. package/dist/errors/HttpError.js +61 -0
  19. package/dist/errors/HttpError.js.map +1 -0
  20. package/dist/health/healthController.d.ts +21 -0
  21. package/dist/health/healthController.d.ts.map +1 -0
  22. package/dist/health/healthController.js +56 -0
  23. package/dist/health/healthController.js.map +1 -0
  24. package/dist/index.d.ts +20 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +81 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/logging/logger.d.ts +4 -0
  29. package/dist/logging/logger.d.ts.map +1 -0
  30. package/dist/logging/logger.js +78 -0
  31. package/dist/logging/logger.js.map +1 -0
  32. package/dist/metrics/index.d.ts +17 -0
  33. package/dist/metrics/index.d.ts.map +1 -0
  34. package/dist/metrics/index.js +99 -0
  35. package/dist/metrics/index.js.map +1 -0
  36. package/dist/middleware/authMiddleware.d.ts +9 -0
  37. package/dist/middleware/authMiddleware.d.ts.map +1 -0
  38. package/dist/middleware/authMiddleware.js +32 -0
  39. package/dist/middleware/authMiddleware.js.map +1 -0
  40. package/dist/middleware/corsMiddleware.d.ts +20 -0
  41. package/dist/middleware/corsMiddleware.d.ts.map +1 -0
  42. package/dist/middleware/corsMiddleware.js +33 -0
  43. package/dist/middleware/corsMiddleware.js.map +1 -0
  44. package/dist/middleware/errorHandler.d.ts +14 -0
  45. package/dist/middleware/errorHandler.d.ts.map +1 -0
  46. package/dist/middleware/errorHandler.js +88 -0
  47. package/dist/middleware/errorHandler.js.map +1 -0
  48. package/dist/middleware/requestLogger.d.ts +16 -0
  49. package/dist/middleware/requestLogger.d.ts.map +1 -0
  50. package/dist/middleware/requestLogger.js +49 -0
  51. package/dist/middleware/requestLogger.js.map +1 -0
  52. package/dist/middleware/security.d.ts +26 -0
  53. package/dist/middleware/security.d.ts.map +1 -0
  54. package/dist/middleware/security.js +47 -0
  55. package/dist/middleware/security.js.map +1 -0
  56. package/dist/middleware/validate.d.ts +11 -0
  57. package/dist/middleware/validate.d.ts.map +1 -0
  58. package/dist/middleware/validate.js +24 -0
  59. package/dist/middleware/validate.js.map +1 -0
  60. package/dist/tracing/index.d.ts +13 -0
  61. package/dist/tracing/index.d.ts.map +1 -0
  62. package/dist/tracing/index.js +89 -0
  63. package/dist/tracing/index.js.map +1 -0
  64. package/dist/types/auth.d.ts +32 -0
  65. package/dist/types/auth.d.ts.map +1 -0
  66. package/dist/types/auth.js +3 -0
  67. package/dist/types/auth.js.map +1 -0
  68. package/dist/utils/correlation.d.ts +11 -0
  69. package/dist/utils/correlation.d.ts.map +1 -0
  70. package/dist/utils/correlation.js +23 -0
  71. package/dist/utils/correlation.js.map +1 -0
  72. package/dist/utils/response.d.ts +24 -0
  73. package/dist/utils/response.d.ts.map +1 -0
  74. package/dist/utils/response.js +35 -0
  75. package/dist/utils/response.js.map +1 -0
  76. package/jest.config.js +14 -0
  77. package/package.json +61 -0
  78. package/src/app/createBaseApp.test.ts +69 -0
  79. package/src/app/createBaseApp.ts +75 -0
  80. package/src/config/env.test.ts +28 -0
  81. package/src/config/env.ts +13 -0
  82. package/src/database/connection.test.ts +142 -0
  83. package/src/database/connection.ts +46 -0
  84. package/src/errors/HttpError.test.ts +38 -0
  85. package/src/errors/HttpError.ts +57 -0
  86. package/src/health/healthController.test.ts +91 -0
  87. package/src/health/healthController.ts +56 -0
  88. package/src/index.ts +60 -0
  89. package/src/logging/logger.test.ts +76 -0
  90. package/src/logging/logger.ts +103 -0
  91. package/src/metrics/index.test.ts +91 -0
  92. package/src/metrics/index.ts +110 -0
  93. package/src/middleware/authMiddleware.test.ts +104 -0
  94. package/src/middleware/authMiddleware.ts +29 -0
  95. package/src/middleware/corsMiddleware.test.ts +58 -0
  96. package/src/middleware/corsMiddleware.ts +36 -0
  97. package/src/middleware/errorHandler.test.ts +146 -0
  98. package/src/middleware/errorHandler.ts +98 -0
  99. package/src/middleware/requestLogger.test.ts +81 -0
  100. package/src/middleware/requestLogger.ts +47 -0
  101. package/src/middleware/security.test.ts +45 -0
  102. package/src/middleware/security.ts +43 -0
  103. package/src/middleware/validate.test.ts +60 -0
  104. package/src/middleware/validate.ts +23 -0
  105. package/src/tracing/index.test.ts +250 -0
  106. package/src/tracing/index.ts +97 -0
  107. package/src/types/auth.ts +33 -0
  108. package/src/utils/correlation.test.ts +47 -0
  109. package/src/utils/correlation.ts +22 -0
  110. package/src/utils/response.test.ts +64 -0
  111. package/src/utils/response.ts +60 -0
  112. package/tsconfig.build.json +4 -0
  113. 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
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
4
+ }
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
+ }