From 1bc97767559f8f1603eaf7218e9d197602b4fed0 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 24 May 2025 17:48:18 -0500 Subject: [PATCH] feat: implement injectServices middleware and useService helper --- src/index.ts | 47 +++++++++++++- tests/index.spec.ts | 152 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 197 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index c10834b..e2eb7dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,46 @@ -console.log("hono-netdi"); +import { + ServiceScope, + type IServiceProvider, + type IServiceScope, + type ServiceIdentifier, +} from '@stevanfreeborn/netdi'; + +import type { Context, MiddlewareHandler } from 'hono'; + +const SERVICE_SCOPE_KEY = 'serviceScope'; + +declare module 'hono' { + interface ContextVariableMap { + [SERVICE_SCOPE_KEY]?: IServiceScope; + } +} + +export function injectServices(serviceProvider: IServiceProvider): MiddlewareHandler { + return async (c: Context, next: () => Promise) => { + const scope = serviceProvider.createScope(); + + c.set(SERVICE_SCOPE_KEY, scope); + + try { + await next(); + } finally { + scope.dispose(); + } + }; +} + +export function useService(c: Context, serviceType: ServiceIdentifier): T { + const scope = c.get(SERVICE_SCOPE_KEY); + + if (!scope) { + throw new Error( + 'Service scope not found in context. Did you add the injectServices middleware?', + ); + } + + if (scope instanceof ServiceScope === false) { + throw new Error('Service scope is not an instance of ServiceScope.'); + } + + return scope.serviceProvider.getService(serviceType); +} diff --git a/tests/index.spec.ts b/tests/index.spec.ts index d63af00..ce4d4f9 100644 --- a/tests/index.spec.ts +++ b/tests/index.spec.ts @@ -1 +1,151 @@ -console.log("hono-netdi tests"); +import { describe, expect, test } from 'vitest'; +import { Env, Hono } from 'hono'; +import { createServiceIdentifier, IServiceScope, ServiceCollection } from '@stevanfreeborn/netdi'; +import { injectServices, useService } from '../src/index'; +import { randomUUID } from 'crypto'; + +describe('injectServices', () => { + test('it should create a service scope and dispose of it after the request', async () => { + interface ITestService { + id(): string; + } + + class TestService implements ITestService { + constructor() {} + + public id() { + return randomUUID(); + } + } + + const serviceId = createServiceIdentifier(); + const services = new ServiceCollection(); + services.addScoped(serviceId, TestService); + + const sp = services.build(); + const app = new Hono(); + + app.use(injectServices(sp)); + + app.get('/', c => { + const scope = c.get('serviceScope'); + const testService = scope!.serviceProvider.getService(serviceId); + return c.json({ id: testService.id() }); + }); + + const res1 = await app.request('/'); + const json1 = await res1.json(); + const firstId = json1.id; + + const res2 = await app.request('/'); + const json2 = await res2.json(); + const secondId = json2.id; + + expect(firstId).not.toEqual(secondId); + }); +}); + +describe('useService', () => { + test('it should throw an error if the service scope is not found in context', async () => { + type ITestService = object; + + const serviceId = createServiceIdentifier(); + const app = new Hono(); + + app.get('/', c => { + try { + useService(c, serviceId); + return c.text('Service scope found', 200); + } catch { + return c.text('Service scope not found', 500); + } + }); + + const res = await app.request('/'); + + expect(res.status).toBe(500); + }); + + test('it should throw an error if the service scope is not an instance of ServiceScope', async () => { + type ITestService = object; + + const serviceId = createServiceIdentifier(); + const app = new Hono(); + + app.get('/', c => { + c.set('serviceScope', {} as IServiceScope); + + try { + useService(c, serviceId); + return c.text('Service scope found', 200); + } catch { + return c.text('Service scope not found', 500); + } + }); + + const res = await app.request('/'); + + expect(res.status).toBe(500); + }); + + test('it should throw an error if the service is not registered in the service scope', async () => { + interface ITestService { + id(): string; + } + + const serviceId = createServiceIdentifier(); + const services = new ServiceCollection(); + + const sp = services.build(); + const app = new Hono(); + + app.use(injectServices(sp)); + + app.get('/', c => { + try { + useService(c, serviceId); + return c.text('Service found', 200); + } catch { + return c.text('Service not found', 500); + } + }); + + const res = await app.request('/'); + + expect(res.status).toBe(500); + }); + + + test('it should return the service from the service scope', async () => { + interface ITestService { + id(): string; + } + + class TestService implements ITestService { + constructor() {} + + public id() { + return 'test-id'; + } + } + + const serviceId = createServiceIdentifier(); + const services = new ServiceCollection(); + services.addScoped(serviceId, TestService); + + const sp = services.build(); + const app = new Hono(); + + app.use(injectServices(sp)); + + app.get('/', c => { + const testService = useService(c, serviceId); + return c.json({ id: testService.id() }); + }); + + const res = await app.request('/'); + const data = await res.json(); + + expect(data.id).toBe('test-id'); + }); +});