Skip to content

Commit d6fd7ae

Browse files
Holzchopfchenkins
andauthored
chore(backend): refactor interfaces (#166)
* Add NoNever utility type * Refactor api-endpoints * Update ts/common/api-endpoints.ts Co-authored-by: Christian Eichenberger <[email protected]> --------- Co-authored-by: Christian Eichenberger <[email protected]>
1 parent c5a97d8 commit d6fd7ae

File tree

2 files changed

+100
-51
lines changed

2 files changed

+100
-51
lines changed

ts/common/api-endpoints.ts

+93-51
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,121 @@
11
import { ApiRequest } from './api-request'
22
import { ApiResponse } from './api-response'
33
import { Benchmark, BenchmarkPreview, Result, Submission, SubmissionPreview, Test } from './interfaces'
4-
import { Empty, json, ResourceId, StripLocator } from './utility-types'
4+
import { Empty, json, NoNever, ResourceId, StripLocator } from './utility-types'
55

66
/**
7-
* Base interface for registered API GET endpoints.
8-
* @param Query Type of request query. Use `Empty` to indicate absence.
9-
* @param Response Type of response body. Use `null` or `Empty` to indicate absence.
7+
* Interface for registered enpoints. Types the request body, request query
8+
* and response.
109
*/
11-
export interface ApiGetEndpoint<Query, Response> {
12-
request: ApiRequest<Empty, Query>
10+
interface ApiEndpoint<Body = unknown, Query = unknown, Response = unknown> {
11+
request: ApiRequest<Body, Query>
1312
response: ApiResponse<Response>
1413
}
1514

1615
/**
17-
* Base interface for registered API POST endpoints.
18-
* @param Body Type of request body. Use `Empty` to indicate absence.
19-
* @param Response Type of response body. Use `null` or `Empty` to indicate absence.
16+
* Registered endpoints per verb.
17+
* Pairs of `Verb : ApiEndpoint`.
18+
* @see {@link ApiEndpoint}
2019
*/
21-
export interface ApiPostEndpoint<Body, Response> {
22-
request: ApiRequest<Body, Empty>
23-
response: ApiResponse<Response>
20+
interface ApiEndpointVerbs {
21+
GET?: ApiEndpoint
22+
POST?: ApiEndpoint
23+
PATCH?: ApiEndpoint
2424
}
2525

26+
// There is no way in TypeScript to enforce the types of an interface's
27+
// properties a priori - instead, give dev freedom to make mistakes and filter
28+
// those endpoints that actually match the `path: ApiEndpointVerbs` pattern
29+
// below (see ApiEndpoints below), otherwise any mistake in the definitions
30+
// would cause hard-to-track-down type errors in controllers.
2631
/**
27-
* Base interface for registered API PATCH endpoints.
28-
* @param Body Type of request body. Use `Empty` to indicate absence.
29-
* @param Response Type of response body. Use `null` or `Empty` to indicate absence.
32+
* Registered API endpoints.
33+
* Pairs of `path : ApiEndpointVerbs` with `path` being a string starting with
34+
* `/`. Use `:<name>` syntax to set up named parameters.
35+
* @see {@link ApiEndpointVerbs}
36+
* @see {@link ApiEndpoint}
3037
*/
31-
export interface ApiPatchEndpoint<Body, Response> {
32-
request: ApiRequest<Body, Empty>
33-
response: ApiResponse<Response>
38+
interface ApiEndpointDefinitions {
39+
'/mirror': {
40+
GET: ApiEndpoint<Empty, Empty, string>
41+
POST: ApiEndpoint<{ data: json }, Empty, { data: json }>
42+
}
43+
'/mirror/:id': {
44+
GET: ApiEndpoint<Empty, Empty, string>
45+
}
46+
'/dbsetup': {
47+
GET: ApiEndpoint<Empty, Empty, null>
48+
}
49+
'/ampq': {
50+
GET: ApiEndpoint<Empty, Empty, string>
51+
POST: ApiEndpoint<json, Empty, string>
52+
}
53+
'/whoami': {
54+
GET: ApiEndpoint<Empty, Empty, json>
55+
}
56+
'/benchmarks': {
57+
GET: ApiEndpoint<Empty, Empty, BenchmarkPreview[]>
58+
}
59+
'/benchmarks/:id': {
60+
GET: ApiEndpoint<Empty, Empty, Benchmark[]>
61+
}
62+
'/tests/:id': {
63+
GET: ApiEndpoint<Empty, Empty, Test[]>
64+
}
65+
'/submissions': {
66+
GET: ApiEndpoint<Empty, { benchmark?: ResourceId; uuid?: string; submitted_by?: string }, SubmissionPreview[]>
67+
POST: ApiEndpoint<StripLocator<Submission>, Empty, { uuid: string }>
68+
}
69+
'/submissions/:uuid': {
70+
GET: ApiEndpoint<Empty, Empty, Submission[]>
71+
}
72+
'/submissions/:uuid/results': {
73+
GET: ApiEndpoint<Empty, Empty, Result[]>
74+
}
75+
'/test': {
76+
GET: ApiEndpoint<Empty, Empty, Test>
77+
}
78+
'/result': {
79+
PATCH: ApiEndpoint<Partial<Result>, Empty, Result>
80+
}
3481
}
3582

83+
/**
84+
* Type-saved version of `ApiEndpointDefinitions` with all non-conforming defs
85+
* removed.
86+
*/
87+
export type ApiEndpoints = NoNever<{
88+
[E in keyof ApiEndpointDefinitions]: ApiEndpointDefinitions[E] extends ApiEndpointVerbs
89+
? ApiEndpointDefinitions[E]
90+
: never
91+
}>
92+
93+
/**
94+
* Registered API endpoints for given method.
95+
* Extracted from `ApiEndpoints`
96+
* @see {@link ApiEndpoints}
97+
*/
98+
export type ApiEndpointsOfVerb<V extends string> = NoNever<{
99+
[EP in keyof ApiEndpoints]: ApiEndpoints[EP] extends Record<V, ApiEndpoint> ? ApiEndpoints[EP][V] : never
100+
}>
101+
36102
/**
37103
* Registered API endpoints for GET method.
38-
* Pairs of `path : ApiGetEndpoint<Query, Response>` with `path` being a string
39-
* starting with `/`. Use `:<name>` syntax to set up named parameters.
40-
* @see {@link ApiGetEndpoint}
104+
* Extracted from `ApiEndpoints`
105+
* @see {@link ApiEndpoints}
41106
*/
42-
// list endpoints return locators instead of full resources - dev.001
43-
// resource endpoints always return an array of said resource - dev.002
44-
export interface ApiGetEndpoints {
45-
'/mirror': ApiGetEndpoint<Empty, string>
46-
'/mirror/:id': ApiGetEndpoint<Empty, string>
47-
'/dbsetup': ApiGetEndpoint<Empty, unknown>
48-
'/ampq': ApiGetEndpoint<Empty, string>
49-
'/whoami': ApiGetEndpoint<Empty, json>
50-
'/benchmarks': ApiGetEndpoint<Empty, BenchmarkPreview[]>
51-
'/benchmarks/:id': ApiGetEndpoint<Empty, Benchmark[]>
52-
'/tests/:id': ApiGetEndpoint<Empty, Test[]>
53-
'/submissions': ApiGetEndpoint<{ benchmark?: ResourceId; uuid?: string; submitted_by?: string }, SubmissionPreview[]>
54-
'/submissions/:uuid': ApiGetEndpoint<Empty, Submission[]>
55-
'/submissions/:uuid/results': ApiGetEndpoint<Empty, Result[]>
56-
'/test': ApiGetEndpoint<Empty, Empty>
57-
}
107+
export type ApiGetEndpoints = ApiEndpointsOfVerb<'GET'>
58108

59109
/**
60110
* Registered API endpoints for POST method.
61-
* Pairs of `path : ApiPostEndpoint<Body, Response>` with `path` being a string
62-
* starting with `/`. Use `:<name>` syntax to set up named parameters.
63-
* @see {@link ApiPostEndpoint} for syntax.
111+
* Extracted from `ApiEndpoints`
112+
* @see {@link ApiEndpoints}
64113
*/
65-
export interface ApiPostEndpoints {
66-
'/mirror': ApiPostEndpoint<{ data: unknown }, unknown>
67-
'/ampq': ApiPostEndpoint<json, string>
68-
'/submissions': ApiPostEndpoint<StripLocator<Submission>, { uuid: string }>
69-
}
114+
export type ApiPostEndpoints = ApiEndpointsOfVerb<'POST'>
70115

71116
/**
72117
* Registered API endpoints for PATCH method.
73-
* Pairs of `path : ApiPatchEndpoint<Body, Response>` with `path` being a string
74-
* starting with `/`. Use `:<name>` syntax to set up named parameters.
75-
* @see {@link ApiPatchEndpoints} for syntax.
118+
* Extracted from `ApiEndpoints`
119+
* @see {@link ApiEndpoints}
76120
*/
77-
export interface ApiPatchEndpoints {
78-
'/result': ApiPatchEndpoint<Partial<Result>, Result>
79-
}
121+
export type ApiPatchEndpoints = ApiEndpointsOfVerb<'PATCH'>

ts/common/utility-types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ export type BanEmpty<T> =
2121
[K in keyof T as Empty extends Required<T[K]> ? never : K]: T[K]
2222
}
2323

24+
/**
25+
* Utility type removing all properties with type `never`.
26+
*/
27+
export type NoNever<T> = {
28+
[K in keyof T as T[K] extends never ? never : K]: T[K]
29+
}
30+
2431
/**
2532
* Guard type checking that `T` is not a key of `R`.
2633
*/

0 commit comments

Comments
 (0)