Skip to content

Commit 6a4a41f

Browse files
committed
feat: api type improvements
1 parent 2f8c57e commit 6a4a41f

10 files changed

Lines changed: 56 additions & 38 deletions

File tree

apps/website/src/data/client.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import type { GetAMAsQuery, GetAuthMeQuery, InferAPIRouteResult } from '@chatsift/api';
3+
import type { CreateAMABody, GetAMAsQuery, GetAuthMeQuery, InferAPIRouteResult } from '@chatsift/api';
44
import type { QueryClient } from '@tanstack/react-query';
55
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
66
import type { GettableRoutes, MakeOptions } from './common';
@@ -25,6 +25,7 @@ function make<Options extends MakeOptions & { path: GettableRoutes }>({ path, qu
2525
const fetcher = useClientSideFetcher({ path: finalPath as `/${string}`, method: 'GET' });
2626
return useQuery({
2727
queryKey,
28+
// @ts-expect-error - This won't ever compile
2829
queryFn: async () => fetcher() as Promise<InferAPIRouteResult<Options['path'], 'GET'> | null>,
2930
throwOnError: clientSideErrorHandler({ throwOverride: false }),
3031
refetchOnWindowFocus: false,
@@ -76,12 +77,21 @@ function makeMutation<Options extends MakeOptions, Method extends 'DELETE' | 'PA
7677

7778
export const client = {
7879
auth: {
79-
useMe: (query?: GetAuthMeQuery) => make(routesInfo.auth.me(query ?? { force_fresh: false }))(),
80+
useMe: (query?: GetAuthMeQuery) => make(routesInfo.auth.me(query ?? { force_fresh: 'false' }))(),
8081
useLogout: makeMutation(routesInfo.auth.logout, 'POST', async (queryClient) => queryClient.invalidateQueries()),
8182
},
8283

8384
guilds: {
8485
ama: {
86+
createAMA: (guildId: string) =>
87+
makeMutation(routesInfo.guilds(guildId).ama.amas(), 'POST', async (queryClient) => {
88+
await queryClient.invalidateQueries({
89+
queryKey: [
90+
routesInfo.guilds(guildId).ama.amas({ include_ended: 'false' }).queryKey,
91+
routesInfo.guilds(guildId).ama.amas({ include_ended: 'true' }).queryKey,
92+
],
93+
});
94+
})(),
8595
useAMAs: (guildId: string, query: GetAMAsQuery) => make(routesInfo.guilds(guildId).ama.amas(query))(),
8696
},
8797
},

apps/website/src/data/common.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ export const routesInfo = {
4747

4848
guilds: (guildId: string) => ({
4949
ama: {
50-
amas: (query: GetAMAsQuery) => ({
51-
queryKey: ['guilds', guildId, 'ama', 'amas', String(query.include_ended)],
50+
amas: (query?: GetAMAsQuery) => ({
51+
queryKey: ['guilds', guildId, 'ama', 'amas', String(query?.include_ended ?? false)],
5252
path: '/v3/guilds/:guildId/ama/amas',
53-
query,
53+
query: { include_ended: query?.include_ended ?? 'false' },
5454
params: { guildId },
5555
}),
5656
},

services/api/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type * from './routes/_types/index.js';
1717
export async function bin(): Promise<void> {
1818
const app = polka({
1919
onError(err, req, res) {
20-
context.logger.error({ err, trackingId: (req as TRequest<unknown>).trackingId }, 'request error');
20+
context.logger.error({ err, trackingId: (req as TRequest<any>).trackingId }, 'request error');
2121

2222
if (res.writableEnded) {
2323
return;

services/api/src/routes/_types/routeTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export type * from '../ama/createAMA.js';
12
export type * from '../ama/getAMAs.js';
23

34
export type * from '../auth/discord.js';

services/api/src/routes/ama/createAMA.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ const bodySchema = z.intersection(
4444
promptSchema,
4545
);
4646

47-
export type CreateAMABody = z.infer<typeof bodySchema>;
47+
export type CreateAMABody = z.input<typeof bodySchema>;
4848

4949
export type CreateAMAResult = Selectable<AMASession>;
5050

51-
export default class CreateAMA extends Route<CreateAMAResult, CreateAMABody> {
51+
export default class CreateAMA extends Route<CreateAMAResult, typeof bodySchema> {
5252
public readonly info = {
5353
method: RouteMethod.post,
5454
path: '/v3/guilds/:guildId/ama/amas',
@@ -60,8 +60,8 @@ export default class CreateAMA extends Route<CreateAMAResult, CreateAMABody> {
6060
...isAuthed({ fallthrough: false, isGlobalAdmin: false, isGuildManager: true }),
6161
];
6262

63-
public override async handle(req: TRequest<CreateAMABody>, res: Response, next: NextHandler) {
64-
const data = req.body as CreateAMABody;
63+
public override async handle(req: TRequest<typeof bodySchema>, res: Response, next: NextHandler) {
64+
const data = req.body;
6565
const { guildId } = req.params as { guildId: string };
6666

6767
// TODO(DD): Reconsider?

services/api/src/routes/ama/getAMAs.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import type { AMASession } from '@chatsift/core';
22
import type { Selectable } from 'kysely';
33
import type { NextHandler, Response } from 'polka';
4-
import { z } from 'zod';
4+
import { z, input } from 'zod';
55
import { context } from '../../context.js';
66
import { isAuthed } from '../../middleware/isAuthed.js';
77
import type { TRequest } from '../route.js';
88
import { Route, RouteMethod } from '../route.js';
99

1010
const querySchema = z
1111
.object({
12-
include_ended: z.string().pipe(z.coerce.boolean()).default(false),
12+
include_ended: z.stringbool().optional().default(false),
1313
})
1414
.strict();
1515

16-
export type GetAMAsQuery = z.infer<typeof querySchema>;
16+
export type GetAMAsQuery = z.input<typeof querySchema>;
1717

1818
export interface AMASessionWithCount extends Selectable<AMASession> {
1919
questionCount: number;
2020
}
2121

22-
export default class GetAMAs extends Route<AMASessionWithCount[], GetAMAsQuery> {
22+
export default class GetAMAs extends Route<AMASessionWithCount[], typeof querySchema> {
2323
public readonly info = {
2424
method: RouteMethod.get,
2525
path: '/v3/guilds/:guildId/ama/amas',
@@ -31,8 +31,8 @@ export default class GetAMAs extends Route<AMASessionWithCount[], GetAMAsQuery>
3131
...isAuthed({ fallthrough: false, isGlobalAdmin: false, isGuildManager: true }),
3232
];
3333

34-
public override async handle(req: TRequest<GetAMAsQuery>, res: Response, next: NextHandler) {
35-
const { include_ended } = req.query as unknown as GetAMAsQuery;
34+
public override async handle(req: TRequest<typeof querySchema>, res: Response, next: NextHandler) {
35+
const { include_ended } = req.query;
3636
const { guildId } = req.params as { guildId: string };
3737

3838
let query = context.db.selectFrom('AMASession').selectAll().where('guildId', '=', guildId);

services/api/src/routes/auth/discordCallback.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@ const querySchema = z
2020
state: z.string(),
2121
})
2222
.strict();
23-
type Query = z.infer<typeof querySchema>;
2423

25-
export default class GetAuthDiscordCallback extends Route<never, Query> {
24+
export default class GetAuthDiscordCallback extends Route<never, typeof querySchema> {
2625
public readonly info = {
2726
method: RouteMethod.get,
2827
path: '/v3/auth/discord/callback',
@@ -32,13 +31,13 @@ export default class GetAuthDiscordCallback extends Route<never, Query> {
3231

3332
public override readonly middleware = [...isAuthed({ fallthrough: true, isGlobalAdmin: false })];
3433

35-
public override async handle(req: TRequest<never>, res: Response, next: NextHandler) {
34+
public override async handle(req: TRequest<typeof querySchema>, res: Response, next: NextHandler) {
3635
if (req.tokens) {
3736
res.redirect(context.env.FRONTEND_URL);
3837
return res.end();
3938
}
4039

41-
const { code, state: stateQuery } = req.query as Query;
40+
const { code, state: stateQuery } = req.query;
4241

4342
const parsedCookies = cookie.parse(req.headers.cookie ?? '');
4443
if (stateQuery !== parsedCookies['state']) {

services/api/src/routes/auth/me.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ export type { Me, MeGuild } from '../../util/me.js';
1010

1111
const querySchema = z
1212
.object({
13-
force_fresh: z.string().pipe(z.coerce.boolean()).default(false),
13+
force_fresh: z.stringbool().optional().default(false),
1414
})
1515
.strict();
16-
export type GetAuthMeQuery = z.infer<typeof querySchema>;
16+
export type GetAuthMeQuery = z.input<typeof querySchema>;
1717

18-
export default class GetAuthMe extends Route<Me, GetAuthMeQuery> {
18+
export default class GetAuthMe extends Route<Me, typeof querySchema> {
1919
public readonly info = {
2020
method: RouteMethod.get,
2121
path: '/v3/auth/me',
@@ -27,8 +27,8 @@ export default class GetAuthMe extends Route<Me, GetAuthMeQuery> {
2727
...isAuthed({ fallthrough: false, isGlobalAdmin: false, isGuildManager: false }),
2828
];
2929

30-
public override async handle(req: TRequest<never>, res: Response, next: NextHandler) {
31-
const { force_fresh } = req.query as unknown as GetAuthMeQuery;
30+
public override async handle(req: TRequest<typeof querySchema>, res: Response, next: NextHandler) {
31+
const { force_fresh } = req.query;
3232

3333
const result: Me = await fetchMe(req.tokens!.access.discordAccessToken, force_fresh);
3434

services/api/src/routes/route.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { performance } from 'node:perf_hooks';
22
import { setTimeout, clearTimeout } from 'node:timers';
33
import { nanoid } from 'nanoid';
44
import type { IError, Middleware, NextHandler, Polka, Request, Response } from 'polka';
5-
import type { ZodType } from 'zod';
5+
import type { z, ZodType } from 'zod';
66
import { context } from '../context.js';
77
import { jsonParser } from '../middleware/jsonParser.js';
88
import { validate } from '../middleware/validate.js';
@@ -33,13 +33,20 @@ export interface RouteInfo {
3333
}
3434

3535
// TODO: More generics?
36-
export type TRequest<TBody> = Omit<Request, 'body'> & { body: TBody; trackingId: string };
36+
export type TRequest<TBodyOrQueryZodType extends ZodType<any> | never> = Omit<Request, 'body' | 'query'> & {
37+
body: TBodyOrQueryZodType extends never ? never : z.infer<TBodyOrQueryZodType>;
38+
query: TBodyOrQueryZodType extends never ? never : z.infer<TBodyOrQueryZodType>;
39+
trackingId: string;
40+
};
3741

3842
/**
3943
* Represents a route on the server
4044
*/
41-
export abstract class Route<TResult, TBodyOrQuery> {
42-
public readonly __internalOnlyHereForTypeInferrenceDoNotUse__!: { bodyOrQuery: TBodyOrQuery; result: TResult };
45+
export abstract class Route<TResult, TBodyOrQueryZodType extends ZodType<any> | never> {
46+
public readonly __internalOnlyHereForTypeInferrenceDoNotUse__!: {
47+
bodyOrQuery: z.infer<TBodyOrQueryZodType>;
48+
result: TResult;
49+
};
4350

4451
/**
4552
* Base route information
@@ -49,30 +56,30 @@ export abstract class Route<TResult, TBodyOrQuery> {
4956
/**
5057
* Middleware to use for this route - needs to be overriden by subclasses
5158
*/
52-
public readonly middleware: Middleware<TRequest<unknown>>[] = [];
59+
public readonly middleware: Middleware<TRequest<any>>[] = [];
5360

5461
/**
5562
* Schema to use for body validation. Implicitly appends a jsonParser to the middleware
5663
*/
57-
public readonly bodyValidationSchema: ZodType<TBodyOrQuery> | null = null;
64+
public readonly bodyValidationSchema: TBodyOrQueryZodType | null = null;
5865

5966
/**
6067
* Schema to use for query validation.
6168
*/
62-
public readonly queryValidationSchema: ZodType<TBodyOrQuery> | null = null;
69+
public readonly queryValidationSchema: TBodyOrQueryZodType | null = null;
6370

6471
/**
6572
* Handles a request to this route
6673
*/
67-
public abstract handle(req: TRequest<TBodyOrQuery>, res: Response, next: NextHandler): unknown;
74+
public abstract handle(req: TRequest<z.infer<TBodyOrQueryZodType>>, res: Response, next: NextHandler): unknown;
6875

6976
/**
7077
* Registers this route
7178
*
7279
* @param server - The Polka webserver to register this route onto
7380
*/
74-
public register(server: Polka<TRequest<unknown>>): void {
75-
const middleware: Middleware<TRequest<unknown>>[] = [
81+
public register(server: Polka<TRequest<any>>): void {
82+
const middleware: Middleware<TRequest<any>>[] = [
7683
async (req, res, next) => {
7784
req.trackingId = nanoid(10);
7885

@@ -124,7 +131,7 @@ export abstract class Route<TResult, TBodyOrQuery> {
124131
{ trackingId: req.trackingId, method: req.method, path: req.path },
125132
'passing to route handler from middleware',
126133
);
127-
await this.handle(req as TRequest<TBodyOrQuery>, res, next);
134+
await this.handle(req as TRequest<z.infer<TBodyOrQueryZodType>>, res, next);
128135
context.logger.info(
129136
{ trackingId: req.trackingId, method: req.method, path: req.path },
130137
'route handler complete',
@@ -148,8 +155,8 @@ export type ParseHTTPParameters<
148155
export type InferRouteMethod<TRoute extends Route<any, any>> = TRoute['info']['method'];
149156
export type InferRouteResult<TRoute> = TRoute extends Route<infer TResult, any> ? TResult : never;
150157
export type InferRouteBodyOrQuery<TRoute extends Route<any, any>> =
151-
TRoute['bodyValidationSchema'] extends ZodType<infer Body>
158+
TRoute['bodyValidationSchema'] extends ZodType<any, infer Body>
152159
? Body
153-
: TRoute['queryValidationSchema'] extends ZodType<infer Query>
160+
: TRoute['queryValidationSchema'] extends ZodType<any, infer Query>
154161
? Query
155162
: never;

services/api/src/routes/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// This file should exclusively re-export the default exports from each route file
22

3+
export { default as CreateAMA } from './ama/createAMA.js';
34
export { default as GetAMAs } from './ama/getAMAs.js';
45

56
export { default as GetAuthDiscord } from './auth/discord.js';

0 commit comments

Comments
 (0)