Skip to content

Commit 51775f8

Browse files
committed
♻️ complete code unification with set_variables pattern and repository migration
- Unified all remaining API routes to use consistent set_variables pattern - Migrated get_users_list to users repository following factory pattern - Added comprehensive test suite for GetUsersList with snapshots - Updated CONTRIBUTING.md with snapshot testing convention - Request processing stays in middleware, page_title in render functions - Clean architecture: data loading consolidated in loadPageVariables functions
1 parent b0b2486 commit 51775f8

File tree

16 files changed

+557
-164
lines changed

16 files changed

+557
-164
lines changed

CONTRIBUTING.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ Write comprehensive tests following these guidelines:
173173
2. **Database Setup**: Always include `beforeAll(migrate)` and `beforeEach(empty_database)`
174174
3. **Seed Data**: Use unicorn seed functions (e.g., `create_adora_pony_user`, `create_unicorn_organization`)
175175
4. **Snapshots Over Multiple Expects**: Prefer `toMatchInlineSnapshot()` for complex object assertions
176+
5. **Auto-generated Snapshots**: When using `toMatchInlineSnapshot()`, let Bun test write the string value automatically by running the test first with an empty string or no parameter
176177

177178
### Example
178179

@@ -202,13 +203,15 @@ test("returns user with specified columns", async () => {
202203
});
203204
const result = await get_user_by_id(user_id);
204205

206+
// Let Bun auto-generate the snapshot by running test first with empty string
205207
expect(result).toMatchInlineSnapshot(`
206-
{
207-
"email": "adora.pony@unicorn.xyz",
208-
"given_name": "Adora",
209-
"id": ${user_id},
210-
}
211-
`);
208+
{
209+
"email": "adora.pony@unicorn.xyz",
210+
"family_name": "Pony",
211+
"given_name": "Adora",
212+
"id": 1,
213+
}
214+
`);
212215
});
213216
```
214217

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
3+
import { Entity_Schema } from "@~/app.core/schema";
4+
import type { App_Context } from "@~/app.middleware/context";
5+
import type { IdentiteProconnect_PgDatabase } from "@~/identite-proconnect.database";
6+
import { Duplicate_Warning } from "./Duplicate_Warning";
7+
import type { Env } from "hono";
8+
import { useRequestContext } from "hono/jsx-renderer";
9+
import { z } from "zod";
10+
11+
//
12+
13+
export async function loadDuplicateWarningPageVariables(
14+
pg: IdentiteProconnect_PgDatabase,
15+
{ moderation_id, organization_id, user_id }: {
16+
moderation_id: number;
17+
organization_id: number;
18+
user_id: number;
19+
},
20+
) {
21+
const value = await Duplicate_Warning.queryContextValues(pg, {
22+
moderation_id,
23+
organization_id,
24+
user_id,
25+
});
26+
27+
return value;
28+
}
29+
30+
//
31+
32+
export interface ContextVariablesType extends Env {
33+
Variables: Awaited<ReturnType<typeof loadDuplicateWarningPageVariables>>;
34+
}
35+
export type ContextType = App_Context & ContextVariablesType;
36+
37+
//
38+
39+
export const QuerySchema = z.object({
40+
organization_id: z.string().pipe(z.coerce.number().int().nonnegative()),
41+
user_id: z.string().pipe(z.coerce.number().int().nonnegative()),
42+
});
43+
44+
export const ParamSchema = Entity_Schema;
45+
46+
//
47+
48+
type PageInputType = {
49+
out: {
50+
param: z.input<typeof ParamSchema>;
51+
query: z.input<typeof QuerySchema>;
52+
};
53+
};
54+
55+
export const usePageRequestContext = useRequestContext<
56+
ContextType,
57+
any,
58+
PageInputType
59+
>;
Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,36 @@
11
//
22

33
import { zValidator } from "@hono/zod-validator";
4-
import { Entity_Schema } from "@~/app.core/schema";
5-
import type { IdentiteProconnect_Pg_Context } from "@~/app.middleware/set_identite_pg";
4+
import { set_variables } from "@~/app.middleware/context/set_variables";
65
import { Hono } from "hono";
76
import { jsxRenderer } from "hono/jsx-renderer";
8-
import { z } from "zod";
97
import { Duplicate_Warning } from "./Duplicate_Warning";
8+
import { loadDuplicateWarningPageVariables, ParamSchema, QuerySchema, type ContextType } from "./context";
109

1110
//
1211

13-
export default new Hono<IdentiteProconnect_Pg_Context>().get(
14-
"/",
15-
jsxRenderer(),
16-
zValidator("param", Entity_Schema),
17-
zValidator(
18-
"query",
19-
z.object({
20-
organization_id: z.string().pipe(z.coerce.number().int().nonnegative()),
21-
user_id: z.string().pipe(z.coerce.number().int().nonnegative()),
22-
}),
23-
),
24-
async function GET({ render, req, var: { identite_pg } }) {
25-
const { id } = req.valid("param");
26-
const { organization_id, user_id } = req.valid("query");
27-
const value = await Duplicate_Warning.queryContextValues(identite_pg, {
28-
moderation_id: id,
29-
organization_id,
30-
user_id,
31-
});
32-
33-
return render(
34-
<Duplicate_Warning.Context.Provider value={value}>
35-
<Duplicate_Warning />
36-
</Duplicate_Warning.Context.Provider>,
37-
);
38-
},
39-
);
12+
export default new Hono<ContextType>()
13+
.get(
14+
"/",
15+
jsxRenderer(),
16+
zValidator("param", ParamSchema),
17+
zValidator("query", QuerySchema),
18+
async function set_variables_middleware({ req, set, var: { identite_pg } }, next) {
19+
const { id: moderation_id } = req.valid("param");
20+
const { organization_id, user_id } = req.valid("query");
21+
const variables = await loadDuplicateWarningPageVariables(identite_pg, {
22+
moderation_id,
23+
organization_id,
24+
user_id,
25+
});
26+
set_variables(set, variables);
27+
return next();
28+
},
29+
async function GET({ render, var: variables }) {
30+
return render(
31+
<Duplicate_Warning.Context.Provider value={variables}>
32+
<Duplicate_Warning />
33+
</Duplicate_Warning.Context.Provider>,
34+
);
35+
},
36+
);

sources/moderations/api/src/context.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
//
22

33
import { Pagination_Schema, type Pagination } from "@~/app.core/schema";
4+
import type { App_Context } from "@~/app.middleware/context";
45
import { z_coerce_boolean } from "@~/app.core/schema/z_coerce_boolean";
56
import { z_empty_string_to_undefined } from "@~/app.core/schema/z_empty_string_to_undefined";
67
import type { GetModerationsListHandler } from "@~/moderations.repository";
78
import { createContext } from "hono/jsx";
9+
import type { Env } from "hono";
10+
import { useRequestContext } from "hono/jsx-renderer";
811
import { z } from "zod";
912

1013
//
@@ -31,6 +34,30 @@ export type GetModerationsListDTO = Awaited<
3134
ReturnType<GetModerationsListHandler>
3235
>;
3336

37+
export async function loadModerationsListPageVariables(
38+
{ pagination, search }: { pagination: Pagination; search: Search },
39+
) {
40+
return {
41+
pagination,
42+
search,
43+
};
44+
}
45+
46+
//
47+
48+
export interface ContextVariablesType extends Env {
49+
Variables: Awaited<ReturnType<typeof loadModerationsListPageVariables>>;
50+
}
51+
export type ContextType = App_Context & ContextVariablesType;
52+
53+
//
54+
55+
export const usePageRequestContext = useRequestContext<
56+
ContextType,
57+
any,
58+
any
59+
>();
60+
3461
export default createContext({
3562
query_moderations_list: {} as Promise<GetModerationsListDTO>,
3663
pagination: {} as Pagination,

sources/moderations/api/src/index.tsx

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,52 @@
33
import { Pagination_Schema } from "@~/app.core/schema";
44
import { Main_Layout } from "@~/app.layout/index";
55
import { authorized } from "@~/app.middleware/authorized";
6-
import type { App_Context } from "@~/app.middleware/context";
6+
import { set_variables } from "@~/app.middleware/context/set_variables";
77
import { Hono } from "hono";
88
import { jsxRenderer } from "hono/jsx-renderer";
99
import { P, match } from "ts-pattern";
1010
import moderation_router from "./:id/index";
11-
import { Search_Schema } from "./context";
11+
import { loadModerationsListPageVariables, Search_Schema, type ContextType } from "./context";
1212
import { Moderations_Page } from "./page";
1313

1414
//
15-
export default new Hono<App_Context>()
15+
export default new Hono<ContextType>()
1616
.use(authorized())
1717

1818
.route("/:id", moderation_router)
19-
.get("/", jsxRenderer(Main_Layout), function GET({ render, req, set }) {
20-
const query = req.query();
19+
.get(
20+
"/",
21+
jsxRenderer(Main_Layout),
22+
async function set_variables_middleware({ req, set }, next) {
23+
const query = req.query();
2124

22-
const search = match(Search_Schema.parse(query, { path: ["query"] }))
23-
.with(
24-
{ search_email: P.not("") },
25-
{ search_siret: P.not("") },
26-
(search) => ({
27-
...search,
28-
hide_join_organization: false,
29-
hide_non_verified_domain: false,
30-
processed_requests: true,
31-
}),
32-
)
33-
.otherwise((search) => search);
25+
const search = match(Search_Schema.parse(query, { path: ["query"] }))
26+
.with(
27+
{ search_email: P.not("") },
28+
{ search_siret: P.not("") },
29+
(search) => ({
30+
...search,
31+
hide_join_organization: false,
32+
hide_non_verified_domain: false,
33+
processed_requests: true,
34+
}),
35+
)
36+
.otherwise((search) => search);
3437

35-
const pagination = match(
36-
Pagination_Schema.safeParse(query, { path: ["query"] }),
37-
)
38-
.with({ success: true }, ({ data }) => data)
39-
.otherwise(() => Pagination_Schema.parse({}));
38+
const pagination = match(
39+
Pagination_Schema.safeParse(query, { path: ["query"] }),
40+
)
41+
.with({ success: true }, ({ data }) => data)
42+
.otherwise(() => Pagination_Schema.parse({}));
4043

41-
set("page_title", "Liste des moderations");
42-
return render(<Moderations_Page pagination={pagination} search={search} />);
43-
});
44+
const variables = await loadModerationsListPageVariables({ pagination, search });
45+
set_variables(set, variables);
46+
return next();
47+
},
48+
function GET({ render, set, var: { pagination, search } }) {
49+
set("page_title", "Liste des moderations");
50+
return render(<Moderations_Page pagination={pagination} search={search} />);
51+
},
52+
);
4453

4554
//

sources/organizations/api/src/:id/members/context.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,51 @@
11
//
22

3-
import type { Pagination } from "@~/app.core/schema";
3+
import { type Pagination } from "@~/app.core/schema";
44
import type { App_Context } from "@~/app.middleware/context";
55
import { urls } from "@~/app.urls";
6-
import { type GetUsersByOrganizationIdDto } from "@~/users.repository";
6+
import type { IdentiteProconnect_PgDatabase } from "@~/identite-proconnect.database";
7+
import {
8+
GetUsersByOrganizationId,
9+
type GetUsersByOrganizationIdDto,
10+
} from "@~/users.repository";
711
import type { Env, InferRequestType } from "hono";
812
import { createContext } from "hono/jsx";
913
import { useRequestContext } from "hono/jsx-renderer";
1014

1115
//
1216

17+
export async function loadMembersPageVariables(
18+
pg: IdentiteProconnect_PgDatabase,
19+
{
20+
organization_id,
21+
pagination,
22+
}: {
23+
organization_id: number;
24+
pagination: Pagination;
25+
},
26+
) {
27+
const query_members_collection = GetUsersByOrganizationId(pg)({
28+
organization_id,
29+
pagination: { ...pagination, page: pagination.page - 1 },
30+
});
31+
32+
return {
33+
organization_id,
34+
pagination,
35+
query_members_collection,
36+
};
37+
}
38+
39+
//
40+
1341
export const Member_Context = createContext({
1442
user: {} as GetUsersByOrganizationIdDto["users"][number],
1543
});
1644

1745
//
1846

1947
export interface ContextVariablesType extends Env {
20-
Variables: {
21-
organization_id: number;
22-
pagination: Pagination;
23-
query_members_collection: Promise<GetUsersByOrganizationIdDto>;
24-
};
48+
Variables: Awaited<ReturnType<typeof loadMembersPageVariables>>;
2549
}
2650
export type ContextType = App_Context & ContextVariablesType;
2751

sources/organizations/api/src/:id/members/index.tsx

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import {
66
Entity_Schema,
77
Pagination_Schema,
88
} from "@~/app.core/schema";
9-
import { GetUsersByOrganizationId } from "@~/users.repository";
9+
import { set_variables } from "@~/app.middleware/context/set_variables";
1010
import { Hono } from "hono";
1111
import { jsxRenderer } from "hono/jsx-renderer";
1212
import { match } from "ts-pattern";
1313
import { z } from "zod";
1414
import organization_member_router from "./:user_id";
1515
import { Table } from "./Table";
16-
import type { ContextType } from "./context";
16+
import { loadMembersPageVariables, type ContextType } from "./context";
1717

1818
//
1919

@@ -32,34 +32,17 @@ export default new Hono<ContextType>()
3232
page_ref: z.string(),
3333
}),
3434
),
35-
36-
function set_organization_id({ req, set }, next) {
35+
async function set_variables_middleware({ req, set, var: { identite_pg } }, next) {
3736
const { id: organization_id } = req.valid("param");
38-
set("organization_id", organization_id);
39-
return next();
40-
},
41-
function set_query_members_collection(
42-
{ req, set, var: { identite_pg, organization_id } },
43-
next,
44-
) {
45-
const query = req.query();
4637
const pagination = match(
47-
Pagination_Schema.safeParse(query, { path: ["query"] }),
38+
Pagination_Schema.safeParse(req.query(), { path: ["query"] }),
4839
)
4940
.with({ success: true }, ({ data }) => data)
5041
.otherwise(() => Pagination_Schema.parse({}));
51-
52-
set("pagination", pagination);
53-
set(
54-
"query_members_collection",
55-
GetUsersByOrganizationId(identite_pg)({
56-
organization_id,
57-
pagination: { ...pagination, page: pagination.page - 1 },
58-
}),
59-
);
42+
const variables = await loadMembersPageVariables(identite_pg, { organization_id, pagination });
43+
set_variables(set, variables);
6044
return next();
6145
},
62-
6346
async function GET({ render }) {
6447
return render(<Table />);
6548
},

0 commit comments

Comments
 (0)