Skip to content

Commit

Permalink
WIP: feat: bff integrated file upload call (#6419)
Browse files Browse the repository at this point in the history
  • Loading branch information
keepview authored Oct 29, 2024
1 parent 7c795b0 commit 62b9afd
Show file tree
Hide file tree
Showing 24 changed files with 569 additions and 18 deletions.
10 changes: 10 additions & 0 deletions .changeset/hot-roses-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@modern-js/create-request': patch
'@modern-js/plugin-express': patch
'@modern-js/main-doc': patch
'@modern-js/plugin-koa': patch
'@modern-js/bff-core': patch
---

feat(bff): integrated file upload call
feat(bff): 支持文件上传一体化调用
95 changes: 95 additions & 0 deletions packages/document/main-doc/docs/en/components/bff-upload.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
BFF combined with runtime framework provides file upload capabilities, supporting integrated calls and pure function manual calls.

### BFF Function

First, create the `api/lambda/upload.ts` file:

```ts title="api/lambda/upload.ts"
export const post = async ({ formData }: {formData: Record<string, any>}) => {
console.info('formData:', formData);
// do somethings
return {
data: {
code: 0,
},
};
};
```
:::tip
The `formData` parameter in the interface processing function can access files uploaded from the client side. It is an `Object` where the keys correspond to the field names used during the upload.
:::


### Integrated Calling

Next, directly import and call the function in `src/routes/upload/page.tsx`:
```tsx title="routes/upload/page.tsx"
import { upload } from '@api/upload';
import React from 'react';

export default (): JSX.Element => {
const [file, setFile] = React.useState<FileList | null>();

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFile(e.target.files);
};

const handleUpload = () => {
if (!file) {
return;
}
upload({
files: {
images: file,
},
});
};

return (
<div>
<input multiple type="file" onChange={handleChange} />
<button onClick={handleUpload}>upload</button>
</div>
);
};
```
:::tip
Note: The input type must be `{ formData: FormData }` for the upload to succeed.
:::


### Manual Calling
You can manually upload files using the `fetch API`, when calling `fetch`, set the `body` as `FormData` type and submit a post request.

```tsx title="routes/upload/page.tsx"
import React from 'react';

export default (): JSX.Element => {
const [file, setFile] = React.useState<FileList | null>();

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFile(e.target.files);
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData();
if (file) {
for (let i = 0; i < file.length; i++) {
formData.append('images', file[i]);
}
await fetch('/api/upload', {
method: 'POST',
body: formData,
});
}
};

return (
<form onSubmit={handleSubmit}>
<input multiple type="file" onChange={handleChange} />
<button type="submit">upload</button>
</form>
);
};
```
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["function", "frameworks", "extend-server", "sdk"]
["function", "frameworks", "extend-server", "sdk", "upload"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# File Upload

import BffUpload from "@site-docs-en/components/bff-upload";

<BffUpload />
97 changes: 97 additions & 0 deletions packages/document/main-doc/docs/zh/components/bff-upload.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
BFF 搭配运行时框架提供了文件上传能力,支持一体化调用及纯函数手动调用。

### BFF 函数

首先创建 `api/lambda/upload.ts` 文件:

```ts title="api/lambda/upload.ts"
export const post = async ({ formData }: {formData: Record<string, any>}) => {
console.info('formData:', formData);
// do somethings
return {
data: {
code: 0,
},
};
};
```
:::tip
通过接口处理函数入参中的 `formData` 可以获取客户端上传的文件。值为 `Object`,key 为上传时的字段名。
:::

### 一体化调用

接着在 `src/routes/upload/page.tsx` 中直接引入函数并调用:
```tsx title="routes/upload/page.tsx"
import { post } from '@api/upload';
import React from 'react';

export default (): JSX.Element => {
const [file, setFile] = React.useState<FileList | null>();

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFile(e.target.files);
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData();
if (file) {
for (let i = 0; i < file.length; i++) {
formData.append('images', file[i]);
}
post({
formData,
});
}
};

return (
<div>
<input multiple type="file" onChange={handleChange} />
<button onClick={handleUpload}>upload</button>
</div>
);
};
```
:::tip
注意:入参类型必须为:`{ formData: FormData }` 才会正确上传。

:::

### 手动上传
可以基于 `fetch API` 手动上传文件,需要在调用 `fetch` 时,将 `body` 设置为 `FormData` 类型并提交 `post` 请求。

```tsx title="routes/upload/page.tsx"
import React from 'react';

export default (): JSX.Element => {
const [file, setFile] = React.useState<FileList | null>();

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFile(e.target.files);
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData();
if (file) {
for (let i = 0; i < file.length; i++) {
formData.append('images', file[i]);
}
await fetch('/api/upload', {
method: 'POST',
body: formData,
});
}
};

return (
<form onSubmit={handleSubmit}>
<input multiple type="file" onChange={handleChange} />
<button type="submit">upload</button>
</form>
);
};

```
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["function", "frameworks", "extend-server", "sdk"]
["function", "frameworks", "extend-server", "sdk", "upload"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 文件上传

import BffUpload from "@site-docs/components/bff-upload";

<BffUpload />
10 changes: 7 additions & 3 deletions packages/server/bff-core/src/client/generateClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,17 @@ export const generateClient = async ({

let handlersCode = '';
for (const handlerInfo of handlerInfos) {
const { name, httpMethod, routePath } = handlerInfo;
const { name, httpMethod, routePath, action } = handlerInfo;
let exportStatement = `var ${name} =`;
if (name.toLowerCase() === 'default') {
exportStatement = 'default';
}
const upperHttpMethod = httpMethod.toUpperCase();

const routeName = routePath;
if (target === 'server') {
if (action) {
handlersCode += `export ${exportStatement} createUploader('${routeName}');`;
} else if (target === 'server') {
handlersCode += `export ${exportStatement} createRequest('${routeName}', '${upperHttpMethod}', process.env.PORT || ${String(
port,
)}, '${httpMethodDecider ? httpMethodDecider : 'functionName'}' ${
Expand All @@ -91,7 +93,9 @@ export const generateClient = async ({
}
}

const importCode = `import { createRequest } from '${requestCreator}';
const importCode = `import { createRequest${
handlerInfos.find(i => i.action) ? ', createUploader' : ''
} } from '${requestCreator}';
${fetcher ? `import { fetch } from '${fetcher}';\n` : ''}`;

return Ok(`${importCode}\n${handlersCode}`);
Expand Down
39 changes: 38 additions & 1 deletion packages/server/bff-core/src/operators/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { z } from 'zod';
import { z } from 'zod';
import { ValidationError } from '../errors/http';
import {
HttpMetadata,
Expand Down Expand Up @@ -213,3 +213,40 @@ export const Redirect = (url: string): Operator<void> => {
},
};
};

export const Upload = <Schema extends z.ZodType>(
urlPath: string,
schema?: Schema,
): Operator<
{
files: z.input<Schema>;
},
{
formData: z.output<Schema>;
}
> => {
const finalSchema = schema || z.any();
return {
name: 'Upload',
metadata({ setMetadata }) {
setMetadata(OperatorType.Trigger, {
type: TriggerType.Http,
path: urlPath,
method: HttpMethod.Post,
action: 'upload',
});
setMetadata(HttpMetadata.Files, finalSchema);
},
async validate(helper, next) {
const {
inputs: { formData: files },
} = helper;

(helper.inputs as any) = {
...helper.inputs,
files: await validateInput(finalSchema, files),
};
return next();
},
};
};
35 changes: 26 additions & 9 deletions packages/server/bff-core/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,25 @@ export class ApiRouter {
originFuncName: string,
handler: ApiHandler,
): APIHandlerInfo | null {
const httpMethod = this.getHttpMethod(originFuncName, handler);
const httpMethod = this.getHttpMethod(
originFuncName,
handler,
) as HttpMethod;
const routeName = this.getRouteName(filename, handler);
const action = this.getAction(handler);
const responseObj: APIHandlerInfo = {
handler,
name: originFuncName,
httpMethod,
routeName,
filename,
routePath: this.getRoutePath(this.prefix, routeName),
};
if (action) {
responseObj.action = action;
}
if (httpMethod && routeName) {
return {
handler,
name: originFuncName,
httpMethod,
routeName,
filename,
routePath: this.getRoutePath(this.prefix, routeName),
};
return responseObj;
}
return null;
}
Expand Down Expand Up @@ -201,6 +209,15 @@ export class ApiRouter {
}
}

public getAction(handler?: ApiHandler): string | undefined {
if (handler) {
const trigger = Reflect.getMetadata(OperatorType.Trigger, handler);
if (trigger?.action) {
return trigger.action;
}
}
}

public loadApiFiles() {
if (!this.existLambdaDir) {
return [];
Expand Down
1 change: 1 addition & 0 deletions packages/server/bff-core/src/router/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export type APIHandlerInfo = {
routeName: string;
// prefix+ routeName
routePath: string;
action?: string;
};
2 changes: 2 additions & 0 deletions packages/server/bff-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum HttpMetadata {
Params = 'PARAMS',
Headers = 'HEADERS',
Response = 'RESPONSE',
Files = 'Files',
}

export enum ResponseMetaType {
Expand All @@ -42,6 +43,7 @@ export type InputSchemaMeata = Extract<
| HttpMetadata.Query
| HttpMetadata.Headers
| HttpMetadata.Params
| HttpMetadata.Files
>;

export type ExecuteFunc<Outputs> = (
Expand Down
Loading

0 comments on commit 62b9afd

Please sign in to comment.