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
18 changes: 18 additions & 0 deletions src/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<IocContainer>(app.ioc);
expectType<{ find: () => string[] }>(await app.ioc.get(usersProvider));
expectType<unknown>(await app.ioc.get("users"));
expectType<{ find: () => string[] }>(
await app.ioc.get<{ find: () => string[] }>("users"),
);
112 changes: 112 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof usersProvider.expose>> | 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();

Expand Down
96 changes: 88 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,14 +27,54 @@ export interface CreateAppOptions {
overrides?: ProviderAny[];
}

export interface IocContainer {
get<ProviderDepsMap extends BaseProviderDepsMap, Value>(
provider: ProviderDef<ProviderDepsMap, Value>,
): Promise<Value>;
get<Value = unknown>(providerName: string): Promise<Value>;
}

declare module "fastify" {
export interface FastifyInstance {
describeTree: () => string;
ioc: IocContainer;
}
}

type InstancesMap = Map<string, string>;

class ApplicationIocContainer implements IocContainer {
constructor(
private readonly container: Container,
private readonly providersByName: Map<string, ProviderAny>,
private readonly providerContexts: Map<ProviderAny, ModuleContext>,
) {}

get<ProviderDepsMap extends BaseProviderDepsMap, Value>(
provider: ProviderDef<ProviderDepsMap, Value>,
): Promise<Value>;
get<Value = unknown>(providerName: string): Promise<Value>;
async get(providerOrName: ProviderAny | string): Promise<unknown> {
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,
Expand All @@ -48,34 +92,65 @@ export async function createApp({
const providerNameToId: InstancesMap = new Map();

const allProviders = new Set<ProviderAny>();
const providerContexts = new Map<ProviderAny, ModuleContext>();
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<string, ProviderAny>();
for (const p of overrides) {
overrideMap.set(p.name, p);
}

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) {
Expand Down Expand Up @@ -127,12 +202,17 @@ function collectProvidersFromConfig(
},
allProviders: Set<ProviderAny>,
providerNameToId: InstancesMap,
providerContexts: Map<ProviderAny, ModuleContext>,
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);
}
});
}
}
Expand Down
Loading