diff --git a/src/index.test-d.ts b/src/index.test-d.ts new file mode 100644 index 0000000..cec8616 --- /dev/null +++ b/src/index.test-d.ts @@ -0,0 +1,18 @@ +import { expectType } from "tsd"; +import { createApp, createModule, createProvider, type IocContainer } from "."; + +const usersProvider = createProvider({ + name: "users", + expose: () => ({ + find: () => ["Alice"], + }), +}); +const root = createModule({ name: "root" }); +const app = await createApp({ root }); + +expectType(app.ioc); +expectType<{ find: () => string[] }>(await app.ioc.get(usersProvider)); +expectType(await app.ioc.get("users")); +expectType<{ find: () => string[] }>( + await app.ioc.get<{ find: () => string[] }>("users"), +); diff --git a/src/index.test.ts b/src/index.test.ts index 4593a7e..67cf571 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -15,6 +15,118 @@ describe("createApp", () => { await app.close(); }); + test("should expose the application container on the Fastify instance", async (t: TestContext) => { + const users = { find: () => ["Alice"] }; + const usersProvider = createProvider({ + name: "users", + expose: () => users, + }); + const usersController = createController({ + name: "users-controller", + deps: { users: usersProvider }, + build: () => {}, + }); + const root = createModule({ + name: "root", + controllers: [usersController], + }); + + const app = await createApp({ root }); + + t.assert.ok(app.hasDecorator("ioc")); + t.assert.strictEqual(await app.ioc.get(usersProvider), users); + t.assert.strictEqual(await app.ioc.get("users"), users); + + await app.close(); + }); + + test("should reject providers that are not registered in the application", async (t: TestContext) => { + let exposeCalled = false; + const usersProvider = createProvider({ + name: "users", + expose: () => { + exposeCalled = true; + return { find: () => ["Alice"] }; + }, + }); + const root = createModule({ name: "root" }); + + const app = await createApp({ root }); + + await t.assert.rejects(() => app.ioc.get(usersProvider), { + message: 'Provider "users" is not registered in the application.', + }); + await t.assert.rejects(() => app.ioc.get("missing-users"), { + message: 'Provider "missing-users" is not registered in the application.', + }); + t.assert.strictEqual(exposeCalled, false); + + await app.close(); + }); + + test("should resolve providers from ioc with application overrides", async (t: TestContext) => { + const usersProvider = createProvider({ + name: "users", + expose: () => ({ find: () => ["real-user"] }), + }); + const fakeUsersProvider = createProvider({ + name: "users", + expose: () => ({ find: () => ["fake-user"] }), + }); + const usersController = createController({ + name: "users-controller", + deps: { users: usersProvider }, + build: () => {}, + }); + const root = createModule({ + name: "root", + controllers: [usersController], + }); + + const app = await createApp({ + root, + overrides: [fakeUsersProvider], + }); + + const users = await app.ioc.get(usersProvider); + t.assert.deepStrictEqual(users.find(), ["fake-user"]); + + await app.close(); + }); + + test("should get providers used by nested modules from ioc", async (t: TestContext) => { + const usersProvider = createProvider({ + name: "users", + expose: () => ({ find: () => ["nested-user"] }), + }); + let nestedUsers: + Awaited> | undefined; + + const usersController = createController({ + name: "users-controller", + deps: { users: usersProvider }, + build: ({ deps }) => { + nestedUsers = deps.users; + }, + }); + const usersModule = createModule({ + name: "users-module", + controllers: [usersController], + }); + const root = createModule({ + name: "root", + subModules: [usersModule], + }); + + const app = await createApp({ root }); + + const users = await app.ioc.get(usersProvider); + t.assert.strictEqual(users, nestedUsers); + t.assert.deepStrictEqual(users.find(), ["nested-user"]); + + await app.close(); + }); + test("should use fastifyInstance, if provided ", async (t: TestContext) => { const fastifyInstance = Fastify(); diff --git a/src/index.ts b/src/index.ts index 35c66cf..82b65cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,11 +10,15 @@ export * from "./controllers"; export * from "./fastify"; import { getProviderId, resolveDeps } from "./providers/providers"; -import type { ProviderAny } from "./providers/providers.types"; +import type { + BaseProviderDepsMap, + ProviderAny, + ProviderDef, +} from "./providers/providers.types"; import { Container } from "./container/container"; import { describeTree } from "./printer/describe-tree"; import { getModuleId, registerModule } from "./modules/module"; -import type { ModuleAny } from "./modules/module.types"; +import type { ModuleAny, ModuleContext } from "./modules/module.types"; export interface CreateAppOptions { root: ModuleAny; @@ -23,14 +27,54 @@ export interface CreateAppOptions { overrides?: ProviderAny[]; } +export interface IocContainer { + get( + provider: ProviderDef, + ): Promise; + get(providerName: string): Promise; +} + declare module "fastify" { export interface FastifyInstance { describeTree: () => string; + ioc: IocContainer; } } type InstancesMap = Map; +class ApplicationIocContainer implements IocContainer { + constructor( + private readonly container: Container, + private readonly providersByName: Map, + private readonly providerContexts: Map, + ) {} + + get( + provider: ProviderDef, + ): Promise; + get(providerName: string): Promise; + async get(providerOrName: ProviderAny | string): Promise { + const provider = + typeof providerOrName === "string" + ? this.providersByName.get(providerOrName) + : providerOrName; + const providerName = + typeof providerOrName === "string" ? providerOrName : providerOrName.name; + const providerContext = provider + ? this.providerContexts.get(provider) + : undefined; + + if (!provider || !providerContext) { + throw new Error( + `Provider "${providerName}" is not registered in the application.`, + ); + } + + return this.container.get(provider, providerContext); + } +} + export async function createApp({ fastifyInstance, serverOptions, @@ -48,20 +92,46 @@ export async function createApp({ const providerNameToId: InstancesMap = new Map(); const allProviders = new Set(); + const providerContexts = new Map(); walkModules(root, (m) => { ensureModuleNameUnicity(moduleNameToId, m); + const moduleContext = { + name: m.name, + bindings: m.bindings, + }; for (const hook of m.hooks) { - collectProvidersFromConfig(hook, allProviders, providerNameToId); + collectProvidersFromConfig( + hook, + allProviders, + providerNameToId, + providerContexts, + moduleContext, + ); } for (const controller of m.controllers) { - collectProvidersFromConfig(controller, allProviders, providerNameToId); + collectProvidersFromConfig( + controller, + allProviders, + providerNameToId, + providerContexts, + moduleContext, + ); } for (const installer of m.installers) { - collectProvidersFromConfig(installer, allProviders, providerNameToId); + collectProvidersFromConfig( + installer, + allProviders, + providerNameToId, + providerContexts, + moduleContext, + ); } }); + const providersByName = new Map( + [...allProviders].map((provider) => [provider.name, provider]), + ); const overrideMap = new Map(); for (const p of overrides) { @@ -69,13 +139,18 @@ export async function createApp({ } const container = new Container(overrideMap); - - await registerModule(fastify, root, container); - const ctx = { name: root.name, bindings: root.bindings, }; + + fastify.decorate( + "ioc", + new ApplicationIocContainer(container, providersByName, providerContexts), + ); + + await registerModule(fastify, root, container); + fastify.addHook("onReady", async () => { for (const prov of allProviders) { if (!prov.onReady) { @@ -127,12 +202,17 @@ function collectProvidersFromConfig( }, allProviders: Set, providerNameToId: InstancesMap, + providerContexts: Map, + moduleContext: ModuleContext, ): void { const deps = config.deps; for (const p of Object.values(deps)) { walkProviders(p as ProviderAny, (pp) => { ensureProviderNameUnicity(providerNameToId, pp); allProviders.add(pp); + if (!providerContexts.has(pp)) { + providerContexts.set(pp, moduleContext); + } }); } }