Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { type RequestHandler } from 'express';
2import { type ParamsDictionary, type Query } from 'express-serve-static-core';
3import { type JsonObject, type JsonValue, type ReadonlyDeep } from 'type-fest';
4
5import { type Dependencies } from '../iocContainer/index.js';
6import { type JSONSchemaV4 } from './json-schema-types.js';
7
8export type RequestHandlerWithBodies<
9 ReqBody extends JsonObject,
10 // @ts-ignore Silence error w/ hitting TS max recursion limit before the type param is actually bound.
11 ResBody extends ReadonlyDeep<JsonValue> | undefined, // undefined is used to indicate a 204 response
12> = RequestHandler<
13 ParamsDictionary,
14 ResBody,
15 ReqBody,
16 Query,
17 Record<string, unknown>
18>;
19
20export type Route<
21 ReqBody extends JsonObject,
22 ResBody extends ReadonlyDeep<JsonValue> | undefined,
23> = {
24 path: string;
25 method: 'get' | 'post' | 'patch' | 'delete';
26 handler: (
27 deps: Dependencies,
28 ) =>
29 | RequestHandlerWithBodies<ReqBody, ResBody>
30 | RequestHandlerWithBodies<ReqBody, ResBody>[];
31 name?: string;
32 bodySchema?: JSONSchemaV4<ReqBody>;
33};
34
35type RouteOpts<ReqBody extends JsonObject> = Pick<
36 Route<ReqBody, ReadonlyDeep<JsonValue> | undefined>,
37 'name' | 'bodySchema'
38>;
39
40function makeRoute<
41 ReqBody extends JsonObject,
42 ResBody extends ReadonlyDeep<JsonValue> | undefined,
43>(
44 method: Route<ReqBody, ResBody>['method'],
45 path: Route<ReqBody, ResBody>['path'],
46 ...rest:
47 | [RouteOpts<ReqBody>, Route<ReqBody, ResBody>['handler']]
48 | [Route<ReqBody, ResBody>['handler']]
49) {
50 return {
51 method,
52 path,
53 ...(typeof rest[0] === 'function'
54 ? { handler: rest[0] }
55 : { ...rest[0], handler: rest[1] }),
56 };
57}
58
59// Some helpers function for making route objects, to bring back the ergonomics
60// of app.get('/', ...handlers) while still getting the benefits of returned
61// route objects (whereas app[method]() just triggers the side effect of
62// registering a route with the internal express router).
63// We don't use partial application here to help TS.
64type MakeRouteBoundArgs<
65 ReqBody extends JsonObject,
66 ResBody extends ReadonlyDeep<JsonValue> | undefined,
67> =
68 | [
69 path: Route<ReqBody, ResBody>['path'],
70 opts: RouteOpts<ReqBody>,
71 handler: Route<ReqBody, ResBody>['handler'],
72 ]
73 | [
74 path: Route<ReqBody, ResBody>['path'],
75 handler: Route<ReqBody, ResBody>['handler'],
76 ];
77
78export const route = {
79 // @ts-ignore Silence error w/ hitting TS max recursion limit before the type param is actually bound.
80 get<ResBody extends ReadonlyDeep<JsonValue> | undefined>(
81 ...args: MakeRouteBoundArgs<never, ResBody>
82 ) {
83 // any cast is to work around https://github.com/microsoft/TypeScript/issues/42508
84 // eslint-disable-next-line @typescript-eslint/no-explicit-any
85 return (makeRoute as any)('get', ...args) as Route<never, ResBody>;
86 },
87 post<
88 ReqBody extends JsonObject,
89 ResBody extends ReadonlyDeep<JsonValue> | undefined,
90 >(...args: MakeRouteBoundArgs<ReqBody, ResBody>) {
91 // any cast is to work around https://github.com/microsoft/TypeScript/issues/42508
92 // eslint-disable-next-line @typescript-eslint/no-explicit-any
93 return (makeRoute as any)('post', ...args) as Route<ReqBody, ResBody>;
94 },
95 patch<
96 ReqBody extends JsonObject,
97 ResBody extends ReadonlyDeep<JsonValue> | undefined,
98 >(...args: MakeRouteBoundArgs<ReqBody, ResBody>) {
99 // any cast is to work around https://github.com/microsoft/TypeScript/issues/42508
100 // eslint-disable-next-line @typescript-eslint/no-explicit-any
101 return (makeRoute as any)('patch', ...args) as Route<ReqBody, ResBody>;
102 },
103 del<ResBody extends ReadonlyDeep<JsonValue> | undefined>(
104 ...args: MakeRouteBoundArgs<never, ResBody>
105 ) {
106 // any cast is to work around https://github.com/microsoft/TypeScript/issues/42508
107 // eslint-disable-next-line @typescript-eslint/no-explicit-any
108 return (makeRoute as any)('delete', ...args) as Route<never, ResBody>;
109 },
110};