Skip to content

Commit 0d7c191

Browse files
committed
✨ introduce awilix
1 parent d1c7b5d commit 0d7c191

31 files changed

+722
-213
lines changed

bun.lockb

2.7 KB
Binary file not shown.

bunfig.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#
22

33
[test]
4-
preload = "@~/config.happydom/register.ts"
4+
preload = [
5+
"@~/config.happydom/register.ts"
6+
]

packages/~/app/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@hono/node-server": "1.12.2",
1313
"@hono/sentry": "1.2.0",
1414
"@~/app.core": "workspace:*",
15+
"@~/app.di": "workspace:*",
1516
"@~/app.layout": "workspace:*",
1617
"@~/app.sentry": "workspace:*",
1718
"@~/auth.api": "workspace:*",

packages/~/app/api/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//
22

33
import config from "@~/app.core/config";
4+
import { asValue, set_injector } from "@~/app.di";
45
import { Root_Layout } from "@~/app.layout/root";
56
import { moncomptepro_pg_database } from "@~/app.middleware/moncomptepro_pg";
67
import { hyyyyyypertool_session } from "@~/app.middleware/session";
@@ -27,6 +28,7 @@ import readyz_router from "./readyz";
2728
//
2829

2930
const app = new Hono()
31+
.use(set_injector())
3032
.use(logger(consola.info))
3133
.use(compress())
3234
.use(set_sentry())
@@ -53,6 +55,12 @@ const app = new Hono()
5355
.route("/auth", auth_router)
5456
//
5557
.use(moncomptepro_pg_database({ connectionString: config.DATABASE_URL }))
58+
.use(({ var: { moncomptepro_pg, injector } }, next) => {
59+
injector.register({
60+
pg: asValue(moncomptepro_pg),
61+
});
62+
return next();
63+
})
5664
//
5765

5866
.route("/moderations", moderations_router)

packages/~/app/api/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
{ "path": "../../users/api/tsconfig.json" },
1414
{ "path": "../../welcome/api/tsconfig.json" },
1515
{ "path": "../core/tsconfig.json" },
16+
{ "path": "../di/tsconfig.json" },
1617
{ "path": "../layout/tsconfig.json" },
1718
{ "path": "../middleware/tsconfig.json" }
1819
]

packages/~/app/di/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@~/app.di",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"exports": {
7+
".": "./src/index.ts",
8+
"./*": {
9+
"default": "./src/*.ts"
10+
}
11+
},
12+
"scripts": {
13+
"clean": "rm -rf node_modules/.cache/tsc",
14+
"format": "prettier --write src/**/*.ts"
15+
},
16+
"dependencies": {
17+
"@~/app.core": "workspace:*",
18+
"awilix": "^11.0.0",
19+
"consola": "3.2.3",
20+
"hono": "4.5.10"
21+
},
22+
"devDependencies": {
23+
"@~/config.typescript": "workspace:*"
24+
}
25+
}

packages/~/app/di/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//
2+
3+
export { asClass, asFunction, asValue } from "awilix";
4+
export { set_injector, set_scope } from "./set_injector";
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
//
2+
3+
import { asFunction, asValue } from "awilix";
4+
import { expect, test } from "bun:test";
5+
import { Hono } from "hono";
6+
import { validator } from "hono/validator";
7+
import { set_injector, set_scope } from "./set_injector";
8+
9+
//
10+
11+
test("should empty todo list", async () => {
12+
const response = await test_app([]).request("/todos");
13+
expect(response.status).toBe(200);
14+
expect(await response.json()).toEqual([]);
15+
});
16+
17+
test("should get todo list", async () => {
18+
const response = await test_app([
19+
{
20+
id: "42",
21+
title: "foo",
22+
is_done: false,
23+
},
24+
]).request("/todos");
25+
expect(response.status).toBe(200);
26+
expect(await response.json()).toEqual([
27+
{
28+
id: "42",
29+
title: "foo",
30+
is_done: false,
31+
},
32+
]);
33+
});
34+
35+
test("should create todo", async () => {
36+
const body = new FormData();
37+
body.append("title", "foo");
38+
const response = await test_app([]).request("/todos", {
39+
method: "POST",
40+
body,
41+
});
42+
expect(response.status).toBe(200);
43+
expect(await response.json()).toEqual({
44+
id: "0",
45+
title: "foo",
46+
is_done: false,
47+
});
48+
});
49+
50+
test("should get todo 42", async () => {
51+
const response = await test_app([
52+
{
53+
id: "42",
54+
title: "foo",
55+
is_done: false,
56+
},
57+
]).request("/todos/42");
58+
expect(response.status).toBe(200);
59+
expect(await response.json()).toEqual({
60+
id: "42",
61+
title: "foo",
62+
is_done: false,
63+
});
64+
});
65+
66+
//
67+
68+
function test_app(initial_todos: Todo[]) {
69+
const todos = new Hono()
70+
.get(
71+
"/",
72+
set_scope<GetTodosCommand>((injector) => {
73+
injector.register({
74+
get_todos: asFunction(in_memory_get_todos_handler),
75+
});
76+
}),
77+
async ({ json, var: { injector } }) => {
78+
const { get_todos } = injector.cradle;
79+
const todos = await get_todos();
80+
return json(todos);
81+
},
82+
)
83+
.post(
84+
"/",
85+
validator("form", (value, c) => {
86+
const title = value["title"];
87+
if (!title || typeof title !== "string") {
88+
return c.text("Invalid!", 400);
89+
}
90+
return value as Pick<Todo, "title">;
91+
}),
92+
set_scope<CreateTodoCommand>((injector) => {
93+
injector.register({
94+
create_todo: asFunction(in_memory_create_todo_handler),
95+
});
96+
}),
97+
async ({ json, req, var: { injector } }) => {
98+
const form = req.valid("form");
99+
const { create_todo } = injector.cradle;
100+
const todo = await create_todo(form.title);
101+
return json(todo);
102+
},
103+
)
104+
.get(
105+
"/:id",
106+
set_scope<GetTodoByIdCommand>((injector) => {
107+
injector.register({
108+
get_todo_by_id: asFunction(in_memory_get_todo_by_id_handler),
109+
});
110+
}),
111+
async ({ json, notFound, req, var: { injector } }) => {
112+
const { get_todo_by_id } = injector.cradle;
113+
const id = req.param("id");
114+
const todo = await get_todo_by_id(id);
115+
if (!todo) return notFound();
116+
return json(todo);
117+
},
118+
);
119+
120+
return new Hono()
121+
.use(
122+
set_injector<InMemoryDatabase>(function root_injector(injector) {
123+
injector.register({
124+
database: asValue({ todos: initial_todos }),
125+
});
126+
}),
127+
)
128+
.route("/todos", todos);
129+
}
130+
131+
//#region CORE
132+
interface Todo {
133+
id: string;
134+
title: string;
135+
is_done: boolean;
136+
}
137+
138+
type CreateTodo = (title: string) => Promise<Todo>;
139+
140+
interface CreateTodoCommand {
141+
create_todo: CreateTodo;
142+
}
143+
144+
type GetTodos = () => Promise<Todo[]>;
145+
interface GetTodosCommand {
146+
get_todos: GetTodos;
147+
}
148+
149+
type GetTodoById = (id: string) => Promise<Todo>;
150+
interface GetTodoByIdCommand {
151+
get_todo_by_id: GetTodoById;
152+
}
153+
//#endregion
154+
155+
//#region INFRA
156+
interface InMemoryDatabase {
157+
database: {
158+
todos: Todo[];
159+
};
160+
}
161+
//#endregion
162+
163+
//#region SHELL
164+
function in_memory_get_todo_by_id_handler({
165+
database,
166+
}: InMemoryDatabase): GetTodoById {
167+
return function get_todos(id) {
168+
const todo = database.todos.find((todo) => todo.id === id);
169+
return todo
170+
? Promise.resolve(todo)
171+
: Promise.reject(new Error("Not found"));
172+
};
173+
}
174+
function in_memory_get_todos_handler({ database }: InMemoryDatabase): GetTodos {
175+
return function get_todos() {
176+
return Promise.resolve(database.todos);
177+
};
178+
}
179+
function in_memory_create_todo_handler({
180+
database,
181+
}: InMemoryDatabase): CreateTodo {
182+
return function create_todo(title) {
183+
const todo = {
184+
id: String(database.todos.length),
185+
title,
186+
is_done: false,
187+
};
188+
database.todos.push(todo);
189+
return Promise.resolve(todo);
190+
};
191+
}
192+
//#endregion
193+
194+
// function in_memory_create_todo_workflow({
195+
// database,
196+
// }: InMemoryDatabase): CreateTodoWorkflow {
197+
// return {
198+
// create_todo(title: string): Promise<Todo> {
199+
// const todo = {
200+
// id: String(database.todos.length),
201+
// title,
202+
// is_done: false,
203+
// };
204+
// database.todos.push(todo);
205+
// return Promise.resolve(todo);
206+
// },
207+
// };
208+
// }
209+
210+
// const app = new Hono()
211+
// .use(
212+
// set_injector((container) => {
213+
// container.register({});
214+
// container.registerInstance(MAIAR_TOKEN, "Maiar");
215+
// container.registerInstance(NARYA_TOKEN, "the Ring of Fire");
216+
// container.registerInstance(NENYA_TOKEN, "the Ring of Air");
217+
// container.registerInstance(VILYA_TOKEN, "the Ring of Water");
218+
// }),
219+
// )
220+
// .get("/races", set_scope(), ({ json, var: { injector } }) =>
221+
// json({
222+
// maiar: injector.resolve(MAIAR_TOKEN),
223+
// }),
224+
// )
225+
// .get("/rings", ({ json, var: { injector } }) =>
226+
// json({
227+
// narya: injector.resolve(NARYA_TOKEN),
228+
// nenya: injector.resolve(NENYA_TOKEN),
229+
// vilya: injector.resolve(VILYA_TOKEN),
230+
// }),
231+
// )
232+
// .post("/rings.battle", set_scope(), async ({ json, var: { injector } }) => {
233+
// // const foo = injector.resolve(Bar);
234+
// // const workflow = injector.resolve(RingBattleWorkflow);
235+
// // await workflow.execute();
236+
// return json({});
237+
// })
238+
// .route("/fr", fr);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
3+
import { createContainer, type AwilixContainer } from "awilix";
4+
import type { Env } from "hono";
5+
import { createMiddleware } from "hono/factory";
6+
7+
//
8+
9+
export function set_injector<TCradle extends object>(
10+
fn?: (injector: AwilixContainer<TCradle>) => void,
11+
) {
12+
type DiEnv = Env & { Variables: { injector: AwilixContainer<TCradle> } };
13+
return createMiddleware<DiEnv>((c, next) => {
14+
const container = createContainer<TCradle>({
15+
strict: true,
16+
});
17+
c.set("injector", container);
18+
if (fn) fn(c.var.injector);
19+
return next();
20+
});
21+
}
22+
23+
export function set_scope<TCradle extends object>(
24+
fn?: (injector: AwilixContainer<TCradle>) => void,
25+
) {
26+
type DiEnv = Env & { Variables: { injector: AwilixContainer<TCradle> } };
27+
return createMiddleware<DiEnv>(async (c, next) => {
28+
if (!c.var.injector)
29+
throw new Error(`"c.var.injector" is required. use set_injector fist`);
30+
const parent_injector = c.var.injector;
31+
const injector = parent_injector.createScope();
32+
c.set("injector", injector);
33+
if (fn) fn(injector);
34+
return next();
35+
});
36+
}

packages/~/app/di/tsconfig.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"types": ["bun-types"],
4+
"outDir": "./node_modules/.cache/tsc"
5+
},
6+
"extends": "@~/config.typescript/base/tsconfig.json",
7+
"references": [{ "path": "../core/tsconfig.json" }]
8+
}

0 commit comments

Comments
 (0)