Skip to content

Commit a36d33a

Browse files
committed
docs(core): 完善函数式API输入schema的类型推导文档与示例
更新文档以说明input()的schema类型会传递到handle()函数中 在多个文档文件中添加相关说明和代码示例 更新类型定义以支持更好的类型推导 添加测试用例验证类型推导功能
1 parent a6ca804 commit a36d33a

6 files changed

Lines changed: 207 additions & 60 deletions

File tree

packages/core/src/functional/api.ts

Lines changed: 149 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -42,34 +42,85 @@ function createNamedFunctionalController(
4242
}[className] as new () => any;
4343
}
4444

45-
export interface FunctionalRouteInput {
46-
params?: unknown;
47-
query?: unknown;
48-
body?: unknown;
49-
headers?: unknown;
45+
export interface FunctionalRouteInput<
46+
TParams = unknown,
47+
TQuery = unknown,
48+
TBody = unknown,
49+
THeaders = unknown,
50+
> {
51+
params?: TParams;
52+
query?: TQuery;
53+
body?: TBody;
54+
headers?: THeaders;
5055
}
5156

52-
export interface FunctionalRouteHandlerArgs {
53-
input: FunctionalRouteInput;
57+
type EmptyFunctionalRouteInput = FunctionalRouteInput<
58+
undefined,
59+
undefined,
60+
undefined,
61+
undefined
62+
>;
63+
64+
type InferSafeParseData<TResult> = Extract<TResult, { success: true }> extends {
65+
data: infer TData;
66+
}
67+
? TData
68+
: unknown;
69+
70+
type InferSchemaValue<TSchema> = [TSchema] extends [undefined]
71+
? unknown
72+
: TSchema extends { parseAsync(value: any): Promise<infer TResult> }
73+
? TResult
74+
: TSchema extends { parse(value: any): infer TResult }
75+
? TResult
76+
: TSchema extends { safeParseAsync(value: any): Promise<infer TResult> }
77+
? InferSafeParseData<TResult>
78+
: TSchema extends { safeParse(value: any): infer TResult }
79+
? InferSafeParseData<TResult>
80+
: unknown;
81+
82+
type InferFunctionalRouteInput<TInput extends FunctionalRouteInput> = ([TInput['params']] extends [undefined]
83+
? { params?: unknown }
84+
: { params: InferSchemaValue<TInput['params']> }) &
85+
([TInput['query']] extends [undefined]
86+
? { query?: unknown }
87+
: { query: InferSchemaValue<TInput['query']> }) &
88+
([TInput['body']] extends [undefined]
89+
? { body?: unknown }
90+
: { body: InferSchemaValue<TInput['body']> }) &
91+
([TInput['headers']] extends [undefined]
92+
? { headers?: unknown }
93+
: { headers: InferSchemaValue<TInput['headers']> });
94+
95+
export interface FunctionalRouteHandlerArgs<
96+
TInput extends FunctionalRouteInput = EmptyFunctionalRouteInput,
97+
> {
98+
input: InferFunctionalRouteInput<TInput>;
5499
ctx: any;
55100
next?: NextFunction;
56101
}
57102

58-
export interface FunctionalRouteDefinition {
103+
export interface FunctionalRouteDefinition<
104+
TInput extends FunctionalRouteInput = EmptyFunctionalRouteInput,
105+
TOutput = unknown,
106+
> {
59107
method: string;
60108
path: string | RegExp;
61-
options: FunctionalRouteOptions;
62-
handle: (args: FunctionalRouteHandlerArgs) => Promise<unknown> | unknown;
109+
options: FunctionalRouteOptions<TInput, TOutput>;
110+
handle: (args: FunctionalRouteHandlerArgs<TInput>) => Promise<unknown> | unknown;
63111
}
64112

65-
export interface FunctionalRouteOptions {
113+
export interface FunctionalRouteOptions<
114+
TInput extends FunctionalRouteInput = EmptyFunctionalRouteInput,
115+
TOutput = unknown,
116+
> {
66117
routerName?: string;
67118
middleware?: any[];
68119
summary?: string;
69120
description?: string;
70121
ignoreGlobalPrefix?: boolean;
71-
input?: FunctionalRouteInput;
72-
output?: unknown;
122+
input?: TInput;
123+
output?: TOutput;
73124
}
74125

75126
export type FunctionalControllerOptions = {
@@ -92,17 +143,37 @@ export interface FunctionalApiModuleMeta {
92143
versionPrefix?: string;
93144
}
94145

95-
export interface RouteBuilder {
96-
input(schema: FunctionalRouteOptions['input']): RouteBuilder;
97-
output(schema: FunctionalRouteOptions['output']): RouteBuilder;
98-
middleware(mw: any[]): RouteBuilder;
99-
meta(options: Omit<FunctionalRouteOptions, 'input' | 'output'>): RouteBuilder;
100-
handle(fn: FunctionalRouteDefinition['handle']): FunctionalRouteDefinition;
146+
export interface RouteBuilder<
147+
TInput extends FunctionalRouteInput = EmptyFunctionalRouteInput,
148+
TOutput = unknown,
149+
> {
150+
input<
151+
TParams = undefined,
152+
TQuery = undefined,
153+
TBody = undefined,
154+
THeaders = undefined,
155+
>(
156+
schema: FunctionalRouteInput<TParams, TQuery, TBody, THeaders>
157+
): RouteBuilder<
158+
FunctionalRouteInput<TParams, TQuery, TBody, THeaders>,
159+
TOutput
160+
>;
161+
output<TNextOutput>(schema: TNextOutput): RouteBuilder<TInput, TNextOutput>;
162+
middleware(mw: any[]): RouteBuilder<TInput, TOutput>;
163+
meta(
164+
options: Omit<FunctionalRouteOptions<TInput, TOutput>, 'input' | 'output'>
165+
): RouteBuilder<TInput, TOutput>;
166+
handle(
167+
fn: FunctionalRouteDefinition<TInput, TOutput>['handle']
168+
): FunctionalRouteDefinition<TInput, TOutput>;
101169
}
102170

103-
interface RouteBuilderInternal extends RouteBuilder {
171+
interface RouteBuilderInternal<
172+
TInput extends FunctionalRouteInput = EmptyFunctionalRouteInput,
173+
TOutput = unknown,
174+
> extends RouteBuilder<TInput, TOutput> {
104175
__isRouteBuilder: true;
105-
__build: () => FunctionalRouteDefinition;
176+
__build: () => FunctionalRouteDefinition<TInput, TOutput>;
106177
}
107178

108179
const HTTP_METHODS = [
@@ -116,12 +187,15 @@ const HTTP_METHODS = [
116187
RequestMethod.ALL,
117188
] as const;
118189

119-
function createRouteBuilder(
190+
function createRouteBuilder<
191+
TInput extends FunctionalRouteInput = EmptyFunctionalRouteInput,
192+
TOutput = unknown,
193+
>(
120194
method: string,
121195
path: string | RegExp = '/'
122-
): RouteBuilderInternal {
123-
const route: Omit<FunctionalRouteDefinition, 'handle'> & {
124-
handle?: FunctionalRouteDefinition['handle'];
196+
): RouteBuilderInternal<TInput, TOutput> {
197+
const route: Omit<FunctionalRouteDefinition<TInput, TOutput>, 'handle'> & {
198+
handle?: FunctionalRouteDefinition<TInput, TOutput>['handle'];
125199
} = {
126200
method,
127201
path,
@@ -130,15 +204,15 @@ function createRouteBuilder(
130204
},
131205
};
132206

133-
const builder: RouteBuilderInternal = {
207+
const builder: RouteBuilderInternal<TInput, TOutput> = {
134208
__isRouteBuilder: true,
135209
input(schema) {
136-
route.options.input = schema;
137-
return builder;
210+
route.options.input = schema as any;
211+
return builder as any;
138212
},
139213
output(schema) {
140-
route.options.output = schema;
141-
return builder;
214+
route.options.output = schema as any;
215+
return builder as any;
142216
},
143217
middleware(mw) {
144218
route.options.middleware = mw || [];
@@ -161,14 +235,16 @@ function createRouteBuilder(
161235
'Functional route is missing handler, call .handle(fn) to finish route definition'
162236
);
163237
}
164-
return route as FunctionalRouteDefinition;
238+
return route as FunctionalRouteDefinition<TInput, TOutput>;
165239
},
166240
};
167241

168242
return builder;
169243
}
170244

171-
function getInputFromContext(ctx: any): FunctionalRouteInput {
245+
function getInputFromContext(
246+
ctx: any
247+
): FunctionalRouteInput<unknown, unknown, unknown, unknown> {
172248
return {
173249
params: ctx?.params,
174250
query: ctx?.query,
@@ -219,12 +295,12 @@ async function runSchemaValidation(
219295
return value;
220296
}
221297

222-
async function validateInput(
223-
schema: FunctionalRouteDefinition['options']['input'],
298+
async function validateInput<TInput extends FunctionalRouteInput>(
299+
schema: FunctionalRouteDefinition<TInput>['options']['input'],
224300
input: FunctionalRouteInput
225-
): Promise<FunctionalRouteInput> {
301+
): Promise<InferFunctionalRouteInput<TInput>> {
226302
if (!schema) {
227-
return input;
303+
return input as InferFunctionalRouteInput<TInput>;
228304
}
229305

230306
return {
@@ -240,16 +316,22 @@ async function validateInput(
240316
input.headers,
241317
'input.headers'
242318
),
243-
};
319+
} as InferFunctionalRouteInput<TInput>;
244320
}
245321

246-
function normalizeRouteDefinition(
322+
function normalizeRouteDefinition<
323+
TInput extends FunctionalRouteInput = EmptyFunctionalRouteInput,
324+
TOutput = unknown,
325+
>(
247326
routeName: string,
248-
routeValue: FunctionalRouteDefinition | RouteBuilderInternal
249-
): FunctionalRouteDefinition {
250-
const route = (routeValue as RouteBuilderInternal)?.__isRouteBuilder
251-
? (routeValue as RouteBuilderInternal).__build()
252-
: (routeValue as FunctionalRouteDefinition);
327+
routeValue:
328+
| FunctionalRouteDefinition<TInput, TOutput>
329+
| RouteBuilderInternal<TInput, TOutput>
330+
): FunctionalRouteDefinition<TInput, TOutput> {
331+
const route = (routeValue as RouteBuilderInternal<TInput, TOutput>)
332+
?.__isRouteBuilder
333+
? (routeValue as RouteBuilderInternal<TInput, TOutput>).__build()
334+
: (routeValue as FunctionalRouteDefinition<TInput, TOutput>);
253335

254336
if (!route || typeof route !== 'object') {
255337
throw new Error(
@@ -270,10 +352,28 @@ function normalizeRouteDefinition(
270352
middleware: [],
271353
...route.options,
272354
},
273-
};
355+
} as FunctionalRouteDefinition<TInput, TOutput>;
274356
}
275357

276-
export function defineApi(
358+
type NormalizeDefinedRoute<T> =
359+
T extends RouteBuilderInternal<infer TInput, infer TOutput>
360+
? FunctionalRouteDefinition<TInput, TOutput>
361+
: T extends FunctionalRouteDefinition<infer TInput, infer TOutput>
362+
? FunctionalRouteDefinition<TInput, TOutput>
363+
: never;
364+
365+
type NormalizeDefinedRoutes<
366+
TRoutes extends Record<string, FunctionalRouteDefinition | RouteBuilderInternal>,
367+
> = {
368+
[K in keyof TRoutes]: NormalizeDefinedRoute<TRoutes[K]>;
369+
};
370+
371+
export function defineApi<
372+
TRoutes extends Record<
373+
string,
374+
FunctionalRouteDefinition<any, any> | RouteBuilderInternal<any, any>
375+
>,
376+
>(
277377
prefix: string,
278378
factory: (api: {
279379
get(path?: string | RegExp): RouteBuilder;
@@ -284,12 +384,12 @@ export function defineApi(
284384
options(path?: string | RegExp): RouteBuilder;
285385
head(path?: string | RegExp): RouteBuilder;
286386
all(path?: string | RegExp): RouteBuilder;
287-
}) => Record<string, FunctionalRouteDefinition | RouteBuilderInternal>,
387+
}) => TRoutes,
288388
controllerOptions: FunctionalControllerOptions = {
289389
middleware: [],
290390
sensitive: true,
291391
}
292-
): Record<string, FunctionalRouteDefinition> {
392+
): NormalizeDefinedRoutes<TRoutes> {
293393
const routeFactory = {
294394
get(path: string | RegExp = '/') {
295395
return createRouteBuilder(RequestMethod.GET, path);
@@ -318,7 +418,7 @@ export function defineApi(
318418
};
319419

320420
const definedRoutes = factory(routeFactory);
321-
const normalizedRoutes: Record<string, FunctionalRouteDefinition> = {};
421+
const normalizedRoutes = {} as Record<string, FunctionalRouteDefinition<any, any>>;
322422
const routeNames = Object.keys(definedRoutes || {});
323423

324424
const FunctionalApiController = createNamedFunctionalController(
@@ -442,5 +542,5 @@ export function defineApi(
442542
writable: false,
443543
});
444544

445-
return normalizedRoutes;
545+
return normalizedRoutes as NormalizeDefinedRoutes<TRoutes>;
446546
}

packages/core/test/functional/api.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,28 @@ describe('test/functional/api.test.ts', function () {
145145
);
146146
});
147147

148+
it('should infer input schema type in handler', async () => {
149+
const paramsSchema = {
150+
parse(value: any): { id: string } {
151+
return value;
152+
},
153+
};
154+
155+
defineApi('/users', api => ({
156+
getUser: api
157+
.get('/:id')
158+
.input({
159+
params: paramsSchema,
160+
})
161+
.handle(async ({ input }) => {
162+
const id: string = input.params.id;
163+
return { id };
164+
}),
165+
}));
166+
167+
expect(true).toBe(true);
168+
});
169+
148170
it('should validate output schema at invoke time', async () => {
149171
const outputSchema = {
150172
parse(value) {

site/docs/functional/api-reference.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,22 @@ export default defineConfiguration({
4444
```ts
4545
// src/server/api/user.api.ts
4646
import { defineApi } from '@midwayjs/core/functional';
47+
import { z } from 'zod';
4748

4849
export const userApi = defineApi('/users', api => ({
49-
getUser: api.get('/:id').handle(async ({ input }) => {
50-
return { id: input.params?.id, name: 'harry' };
51-
}),
50+
getUser: api
51+
.get('/:id')
52+
.input({
53+
params: z.object({ id: z.string() }),
54+
})
55+
.handle(async ({ input }) => {
56+
return { id: input.params.id, name: 'harry' };
57+
}),
5258
}));
5359
```
5460

61+
- 说明:`input(...)` 不只做校验,也会把类型传到 `handle(...)`
62+
5563
## Hooks
5664

5765
### `useContext`
@@ -123,13 +131,19 @@ export const pluginApi = defineApi('/plugin', api => ({
123131
```ts
124132
// src/server/api/user.api.ts
125133
import { defineApi, useInject } from '@midwayjs/core/functional';
134+
import { z } from 'zod';
126135
import { UserService } from '../service/user.service';
127136

128137
export const userApi = defineApi('/users', api => ({
129-
getUser: api.get('/:id').handle(async ({ input }) => {
130-
const userService = await useInject(UserService);
131-
return userService.find(input.params?.id);
132-
}),
138+
getUser: api
139+
.get('/:id')
140+
.input({
141+
params: z.object({ id: z.string() }),
142+
})
143+
.handle(async ({ input }) => {
144+
const userService = await useInject(UserService);
145+
return userService.find(input.params.id);
146+
}),
133147
}));
134148
```
135149

0 commit comments

Comments
 (0)