Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"date-fns": "^4.1.0",
"express": "^4.21.2",
"google-auth-library": "^9.15.1",
"hono": "^4.8.5",
"mathjs": "^14.2.1",
"uuid": "^11.0.5",
"winston": "^3.17.0",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/core/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export const getHookCorrelationId = () => {
return context.get('correlationId');
};

export const setHookContext = (callback: (...args: unknown[]) => void) => {
asyncStorage.run(getHookContext(), callback);
export const setHookContext = <R>(callback: (...args: unknown[]) => R): R => {
return asyncStorage.run(getHookContext(), callback);
};

export const setHookCorrelationId = (correlationId: string) => {
Expand Down
26 changes: 26 additions & 0 deletions src/middlewares/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NextFunction, Request, Response } from 'express';
import type { Context, Next } from 'hono';
import { setHookContext, setHookCorrelationId, setHookTenantId } from '../core/hooks';
import { Uuid } from '../core/uuid';

Expand Down Expand Up @@ -28,3 +29,28 @@ export const setTenantId = (defaultTenantId?: string) => {
next();
};
};

export const setContextHono = async (ctx: Context, next: Next) => {
await setHookContext(() => next());
};

export const setCorrelationIdHono = (ctx: Context, next: Next) => {
const correlationIdHeader = ctx.req.header('x-correlation-id');
let correlationId = correlationIdHeader;

if (!correlationId) {
correlationId = Uuid.random().value;
}

setHookCorrelationId(correlationId);
return next();
};

export const setTenantIdHono = (defaultTenantId?: string) => {
return async (ctx: Context, next: Next) => {
const tenantId = ctx.req.header('x-tenant-id') ?? defaultTenantId;
if (!tenantId) throw new Error('Tenant ID is required, but it is not present in the request headers');
setHookTenantId(tenantId);
await next();
};
};
91 changes: 87 additions & 4 deletions tests/middlewares/context.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import type { Request, Response, NextFunction } from 'express';
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { NextFunction, Request, Response } from 'express';
import type { Context, Next } from 'hono';
import type { Mock } from 'vitest';
import { setContext, setCorrelationId, setTenantId } from '../../src/middlewares/context';
import { describe, expect, it, beforeEach, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as hooks from '../../src/core/hooks';
import { Uuid } from '../../src/core/uuid';
import { setContext, setContextHono, setCorrelationId, setCorrelationIdHono, setTenantId, setTenantIdHono } from '../../src/middlewares/context';

// Mock dependencies
vi.mock('../../src/core/hooks');
vi.mock('../../src/core/uuid');

describe('Context Middleware', () => {
describe('Express Context Middleware', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let mockNext: NextFunction;
Expand Down Expand Up @@ -124,3 +126,84 @@ describe('Context Middleware', () => {
});
});
});

describe('Hono Context Middleware', () => {
let mockContext: Partial<Context>;
let mockNext: Next;

beforeEach(() => {
mockContext = {
req: {
header: vi.fn(),
} as any,
};
mockNext = vi.fn().mockResolvedValue(undefined);
vi.clearAllMocks();
(hooks.setHookContext as Mock).mockImplementation(callback => callback());
});

describe('setContextHono', () => {
it('should call setHookContext and next', async () => {
await setContextHono(mockContext as Context, mockNext);
expect(hooks.setHookContext).toHaveBeenCalled();
expect(mockNext).toHaveBeenCalled();
});
});

describe('setCorrelationIdHono', () => {
it('should use correlation ID from header', async () => {
const testCorrelationId = 'test-correlation-id';
(mockContext.req!.header as Mock).mockReturnValue(testCorrelationId);

await setCorrelationIdHono(mockContext as Context, mockNext);

expect(mockContext.req!.header).toHaveBeenCalledWith('x-correlation-id');
expect(hooks.setHookCorrelationId).toHaveBeenCalledWith(testCorrelationId);
expect(mockNext).toHaveBeenCalled();
});

it('should generate new correlation ID if none provided', async () => {
const mockUuid = 'generated-uuid';
(mockContext.req!.header as Mock).mockReturnValue(undefined);
(Uuid.random as Mock).mockReturnValue({ value: mockUuid });

await setCorrelationIdHono(mockContext as Context, mockNext);

expect(hooks.setHookCorrelationId).toHaveBeenCalledWith(mockUuid);
expect(mockNext).toHaveBeenCalled();
});
});

describe('setTenantIdHono', () => {
const defaultTenantId = 'default-tenant';

it('should use tenant ID from header', async () => {
const testTenantId = 'test-tenant';
(mockContext.req!.header as Mock).mockReturnValue(testTenantId);

const middleware = setTenantIdHono(defaultTenantId);
await middleware(mockContext as Context, mockNext);

expect(mockContext.req!.header).toHaveBeenCalledWith('x-tenant-id');
expect(hooks.setHookTenantId).toHaveBeenCalledWith(testTenantId);
expect(mockNext).toHaveBeenCalled();
});

it('should use default tenant ID if none provided in header', async () => {
(mockContext.req!.header as Mock).mockReturnValue(undefined);

const middleware = setTenantIdHono(defaultTenantId);
await middleware(mockContext as Context, mockNext);

expect(hooks.setHookTenantId).toHaveBeenCalledWith(defaultTenantId);
expect(mockNext).toHaveBeenCalled();
});

it('should throw an error if tenant ID is not provided in header nor default tenant ID', async () => {
(mockContext.req!.header as Mock).mockReturnValue(undefined);
const middleware = setTenantIdHono();
await expect(middleware(mockContext as Context, mockNext))
.rejects.toThrow('Tenant ID is required, but it is not present in the request headers');
});
});
});