From 62b9afd67ebc18bb0130821607bab1351ae28f8b Mon Sep 17 00:00:00 2001 From: keepview <35088742+keepview@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:14:27 +0800 Subject: [PATCH] WIP: feat: bff integrated file upload call (#6419) --- .changeset/hot-roses-end.md | 10 ++ .../docs/en/components/bff-upload.mdx | 95 ++++++++++++++++++ .../guides/advanced-features/bff/_meta.json | 2 +- .../guides/advanced-features/bff/upload.mdx | 5 + .../docs/zh/components/bff-upload.mdx | 97 +++++++++++++++++++ .../guides/advanced-features/bff/_meta.json | 2 +- .../guides/advanced-features/bff/upload.mdx | 5 + .../bff-core/src/client/generateClient.ts | 10 +- .../server/bff-core/src/operators/http.ts | 39 +++++++- packages/server/bff-core/src/router/index.ts | 35 +++++-- packages/server/bff-core/src/router/types.ts | 1 + packages/server/bff-core/src/types.ts | 2 + packages/server/create-request/src/browser.ts | 16 ++- packages/server/create-request/src/node.ts | 12 +++ packages/server/create-request/src/types.ts | 3 + packages/server/create-request/src/utiles.ts | 29 ++++++ packages/server/plugin-express/src/utils.ts | 7 +- tests/integration/bff-express/api/upload.ts | 19 ++++ .../bff-express/src/upload/App.tsx | 74 ++++++++++++++ .../bff-express/tests/index.test.ts | 16 +++ tests/integration/bff-koa/api/upload.ts | 19 ++++ .../bff-koa/src/modern-app-env.d.ts | 1 + .../bff-koa/src/routes/upload/page.tsx | 72 ++++++++++++++ tests/integration/bff-koa/tests/index.test.ts | 16 +++ 24 files changed, 569 insertions(+), 18 deletions(-) create mode 100644 .changeset/hot-roses-end.md create mode 100644 packages/document/main-doc/docs/en/components/bff-upload.mdx create mode 100644 packages/document/main-doc/docs/en/guides/advanced-features/bff/upload.mdx create mode 100644 packages/document/main-doc/docs/zh/components/bff-upload.mdx create mode 100644 packages/document/main-doc/docs/zh/guides/advanced-features/bff/upload.mdx create mode 100644 packages/server/create-request/src/utiles.ts create mode 100644 tests/integration/bff-express/api/upload.ts create mode 100644 tests/integration/bff-express/src/upload/App.tsx create mode 100644 tests/integration/bff-koa/api/upload.ts create mode 100644 tests/integration/bff-koa/src/routes/upload/page.tsx diff --git a/.changeset/hot-roses-end.md b/.changeset/hot-roses-end.md new file mode 100644 index 000000000000..94ccfdd2bf16 --- /dev/null +++ b/.changeset/hot-roses-end.md @@ -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): 支持文件上传一体化调用 diff --git a/packages/document/main-doc/docs/en/components/bff-upload.mdx b/packages/document/main-doc/docs/en/components/bff-upload.mdx new file mode 100644 index 000000000000..0b0bde247a77 --- /dev/null +++ b/packages/document/main-doc/docs/en/components/bff-upload.mdx @@ -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}) => { + 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(); + + const handleChange = (e: React.ChangeEvent) => { + setFile(e.target.files); + }; + + const handleUpload = () => { + if (!file) { + return; + } + upload({ + files: { + images: file, + }, + }); + }; + + return ( +
+ + +
+ ); +}; +``` +:::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(); + + const handleChange = (e: React.ChangeEvent) => { + setFile(e.target.files); + }; + + const handleSubmit = async (e: React.FormEvent) => { + 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 ( +
+ + +
+ ); +}; +``` diff --git a/packages/document/main-doc/docs/en/guides/advanced-features/bff/_meta.json b/packages/document/main-doc/docs/en/guides/advanced-features/bff/_meta.json index b4311c4c1017..adeca65ffbe6 100644 --- a/packages/document/main-doc/docs/en/guides/advanced-features/bff/_meta.json +++ b/packages/document/main-doc/docs/en/guides/advanced-features/bff/_meta.json @@ -1 +1 @@ -["function", "frameworks", "extend-server", "sdk"] +["function", "frameworks", "extend-server", "sdk", "upload"] diff --git a/packages/document/main-doc/docs/en/guides/advanced-features/bff/upload.mdx b/packages/document/main-doc/docs/en/guides/advanced-features/bff/upload.mdx new file mode 100644 index 000000000000..661a5f79016b --- /dev/null +++ b/packages/document/main-doc/docs/en/guides/advanced-features/bff/upload.mdx @@ -0,0 +1,5 @@ +# File Upload + +import BffUpload from "@site-docs-en/components/bff-upload"; + + diff --git a/packages/document/main-doc/docs/zh/components/bff-upload.mdx b/packages/document/main-doc/docs/zh/components/bff-upload.mdx new file mode 100644 index 000000000000..bf5dbde769b3 --- /dev/null +++ b/packages/document/main-doc/docs/zh/components/bff-upload.mdx @@ -0,0 +1,97 @@ +BFF 搭配运行时框架提供了文件上传能力,支持一体化调用及纯函数手动调用。 + +### BFF 函数 + +首先创建 `api/lambda/upload.ts` 文件: + +```ts title="api/lambda/upload.ts" +export const post = async ({ formData }: {formData: Record}) => { + 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(); + + const handleChange = (e: React.ChangeEvent) => { + setFile(e.target.files); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(); + if (file) { + for (let i = 0; i < file.length; i++) { + formData.append('images', file[i]); + } + post({ + formData, + }); + } + }; + + return ( +
+ + +
+ ); +}; +``` +:::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(); + + const handleChange = (e: React.ChangeEvent) => { + setFile(e.target.files); + }; + + const handleSubmit = async (e: React.FormEvent) => { + 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 ( +
+ + +
+ ); +}; + +``` diff --git a/packages/document/main-doc/docs/zh/guides/advanced-features/bff/_meta.json b/packages/document/main-doc/docs/zh/guides/advanced-features/bff/_meta.json index b4311c4c1017..adeca65ffbe6 100644 --- a/packages/document/main-doc/docs/zh/guides/advanced-features/bff/_meta.json +++ b/packages/document/main-doc/docs/zh/guides/advanced-features/bff/_meta.json @@ -1 +1 @@ -["function", "frameworks", "extend-server", "sdk"] +["function", "frameworks", "extend-server", "sdk", "upload"] diff --git a/packages/document/main-doc/docs/zh/guides/advanced-features/bff/upload.mdx b/packages/document/main-doc/docs/zh/guides/advanced-features/bff/upload.mdx new file mode 100644 index 000000000000..4b0932d41344 --- /dev/null +++ b/packages/document/main-doc/docs/zh/guides/advanced-features/bff/upload.mdx @@ -0,0 +1,5 @@ +# 文件上传 + +import BffUpload from "@site-docs/components/bff-upload"; + + diff --git a/packages/server/bff-core/src/client/generateClient.ts b/packages/server/bff-core/src/client/generateClient.ts index b3e147cf556d..e442deb3e50e 100644 --- a/packages/server/bff-core/src/client/generateClient.ts +++ b/packages/server/bff-core/src/client/generateClient.ts @@ -66,7 +66,7 @@ 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'; @@ -74,7 +74,9 @@ export const generateClient = async ({ 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'}' ${ @@ -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}`); diff --git a/packages/server/bff-core/src/operators/http.ts b/packages/server/bff-core/src/operators/http.ts index 6dc62ce5f384..cfd03e76d9e9 100644 --- a/packages/server/bff-core/src/operators/http.ts +++ b/packages/server/bff-core/src/operators/http.ts @@ -1,4 +1,4 @@ -import type { z } from 'zod'; +import { z } from 'zod'; import { ValidationError } from '../errors/http'; import { HttpMetadata, @@ -213,3 +213,40 @@ export const Redirect = (url: string): Operator => { }, }; }; + +export const Upload = ( + urlPath: string, + schema?: Schema, +): Operator< + { + files: z.input; + }, + { + formData: z.output; + } +> => { + 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(); + }, + }; +}; diff --git a/packages/server/bff-core/src/router/index.ts b/packages/server/bff-core/src/router/index.ts index 0ce305236a36..180d6fb27f1f 100644 --- a/packages/server/bff-core/src/router/index.ts +++ b/packages/server/bff-core/src/router/index.ts @@ -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; } @@ -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 []; diff --git a/packages/server/bff-core/src/router/types.ts b/packages/server/bff-core/src/router/types.ts index d9efa6c25387..29d992725216 100644 --- a/packages/server/bff-core/src/router/types.ts +++ b/packages/server/bff-core/src/router/types.ts @@ -20,4 +20,5 @@ export type APIHandlerInfo = { routeName: string; // prefix+ routeName routePath: string; + action?: string; }; diff --git a/packages/server/bff-core/src/types.ts b/packages/server/bff-core/src/types.ts index 5cf367d6d186..bbdfe347f96c 100644 --- a/packages/server/bff-core/src/types.ts +++ b/packages/server/bff-core/src/types.ts @@ -16,6 +16,7 @@ export enum HttpMetadata { Params = 'PARAMS', Headers = 'HEADERS', Response = 'RESPONSE', + Files = 'Files', } export enum ResponseMetaType { @@ -42,6 +43,7 @@ export type InputSchemaMeata = Extract< | HttpMetadata.Query | HttpMetadata.Headers | HttpMetadata.Params + | HttpMetadata.Files >; export type ExecuteFunc = ( diff --git a/packages/server/create-request/src/browser.ts b/packages/server/create-request/src/browser.ts index d460dc305153..23532d7c24e0 100644 --- a/packages/server/create-request/src/browser.ts +++ b/packages/server/create-request/src/browser.ts @@ -5,8 +5,10 @@ import type { BFFRequestPayload, IOptions, RequestCreator, + RequestUploader, Sender, } from './types'; +import { getUploadPayload } from './utiles'; let realRequest: typeof fetch; let realAllowedHeaders: string[]; @@ -36,8 +38,7 @@ export const createRequest: RequestCreator = ( path, method, port, - httpMethodDecider = 'functionName', - // 后续可能要修改,暂时先保留 + httpMethodDecider = 'functionName', // 后续可能要修改,暂时先保留 fetch = originFetch, ) => { const getFinalPath = compile(path, { encode: encodeURIComponent }); @@ -49,6 +50,7 @@ export const createRequest: RequestCreator = ( let body; let finalURL: string; let headers: Record; + if (httpMethodDecider === 'inputParams') { finalURL = path; body = JSON.stringify({ @@ -126,3 +128,13 @@ export const createRequest: RequestCreator = ( return sender; }; + +export const createUploader: RequestUploader = (path: string) => { + const sender: Sender = (...args) => { + const fetcher = realRequest || originFetch; + const { body, headers } = getUploadPayload(args); + return fetcher(path, { method: 'POST', body, headers }); + }; + + return sender; +}; diff --git a/packages/server/create-request/src/node.ts b/packages/server/create-request/src/node.ts index 08402bfeb3ba..f1b5e62ac01e 100644 --- a/packages/server/create-request/src/node.ts +++ b/packages/server/create-request/src/node.ts @@ -7,8 +7,10 @@ import type { BFFRequestPayload, IOptions, RequestCreator, + RequestUploader, Sender, } from './types'; +import { getUploadPayload } from './utiles'; type Fetch = typeof nodeFetch; @@ -127,3 +129,13 @@ export const createRequest: RequestCreator = ( return sender; }; + +export const createUploader: RequestUploader = (path: string) => { + const sender: Sender = (...args) => { + const fetcher = realRequest || originFetch; + const { body, headers } = getUploadPayload(args); + return fetcher(path, { method: 'POST', body, headers }); + }; + + return sender; +}; diff --git a/packages/server/create-request/src/types.ts b/packages/server/create-request/src/types.ts index 568d02af4ba6..bbbb619ca9ac 100644 --- a/packages/server/create-request/src/types.ts +++ b/packages/server/create-request/src/types.ts @@ -9,6 +9,7 @@ export type BFFRequestPayload = { data?: Record; headers?: Record; cookies?: Record; + files?: Record; }; export type Sender = ((...args: any[]) => Promise) & { @@ -23,6 +24,8 @@ export type RequestCreator = ( fetch?: F, ) => Sender; +export type RequestUploader = (path: string) => Sender; + export type IOptions = { request?: F; interceptor?: (request: F) => F; diff --git a/packages/server/create-request/src/utiles.ts b/packages/server/create-request/src/utiles.ts new file mode 100644 index 000000000000..42751992215e --- /dev/null +++ b/packages/server/create-request/src/utiles.ts @@ -0,0 +1,29 @@ +import type { BFFRequestPayload } from './types'; + +export const getUploadPayload = (args: any) => { + const payload: BFFRequestPayload = + typeof args[args.length - 1] === 'object' ? args[args.length - 1] : {}; + + const files = payload.files; + if (!files) { + throw new Error('no files'); + } + + const formdata = new FormData(); + for (const [key, value] of Object.entries(files)) { + if (value instanceof FileList) { + for (let i = 0; i < value.length; i++) { + formdata.append(key, value[i]); + } + } else { + formdata.append(key, value); + } + } + + const body: any = formdata; + const headers: Record = { + accept: `application/json,*/*;q=0.8`, + }; + + return { body, headers }; +}; diff --git a/packages/server/plugin-express/src/utils.ts b/packages/server/plugin-express/src/utils.ts index aa391d4c21e6..b45df6f3223a 100644 --- a/packages/server/plugin-express/src/utils.ts +++ b/packages/server/plugin-express/src/utils.ts @@ -17,6 +17,10 @@ import typeIs from 'type-is'; type Handler = APIHandlerInfo['handler']; +interface RequestWithFiles extends Request { + files: Record; +} + const handleResponseMeta = (res: Response, handler: Handler) => { const responseMeta: ResponseMeta[] = Reflect.getMetadata( HttpMetadata.Response, @@ -146,7 +150,8 @@ const getInputFromRequest = async (request: Request) => { if (typeIs(request, ['application/json'])) { draft.data = request.body; } else if (typeIs(request, ['multipart/form-data'])) { - draft.formData = await resolveFormData(request); + const files = await resolveFormData(request); + draft.formData = files; } else if (typeIs(request, ['application/x-www-form-urlencoded'])) { draft.formUrlencoded = request.body; } else { diff --git a/tests/integration/bff-express/api/upload.ts b/tests/integration/bff-express/api/upload.ts new file mode 100644 index 000000000000..73fa2ecf5eb2 --- /dev/null +++ b/tests/integration/bff-express/api/upload.ts @@ -0,0 +1,19 @@ +import { Api, Upload } from '@modern-js/runtime/server'; +import { z } from 'zod'; + +const FileSchema = z.object({ + images: z.record(z.string(), z.any()), +}); + +export const upload = Api( + Upload('/upload', FileSchema), + async ({ formData }) => { + // do somethings + return { + data: { + code: 0, + file_name: formData.images.name, + }, + }; + }, +); diff --git a/tests/integration/bff-express/src/upload/App.tsx b/tests/integration/bff-express/src/upload/App.tsx new file mode 100644 index 000000000000..7ee5719ab16e --- /dev/null +++ b/tests/integration/bff-express/src/upload/App.tsx @@ -0,0 +1,74 @@ +import { upload } from '@api/upload'; +import React, { useEffect } from 'react'; + +const getMockImage = () => { + const imageData = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='; + const blob = new Blob( + [Uint8Array.from(atob(imageData.split(',')[1]), c => c.charCodeAt(0))], + { type: 'image/png' }, + ); + + return new File([blob], 'mock_image.png', { type: 'image/png' }); +}; + +const Index = (): JSX.Element => { + const [file, setFile] = React.useState(); + const [fileName, setFileName] = React.useState(''); + + const handleChange = (e: React.ChangeEvent) => { + setFile(e.target.files); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(); + if (file) { + for (let i = 0; i < file.length; i++) { + formData.append('images', file[i]); + } + await fetch('/bff-api/upload', { + method: 'POST', + body: formData, + }); + } + }; + + const click = () => { + if (!file) { + return; + } + upload({ + files: { + images: file, + }, + }); + }; + + useEffect(() => { + upload({ + files: { + images: getMockImage(), + }, + }).then(res => { + setFileName(res.data.file_name); + }); + }, []); + + return ( + <> +

File Upload

+

fileName: {fileName}

+
+ + +
+
+ + +
+ + ); +}; + +export default Index; diff --git a/tests/integration/bff-express/tests/index.test.ts b/tests/integration/bff-express/tests/index.test.ts index b04cb5ea910c..614741035679 100644 --- a/tests/integration/bff-express/tests/index.test.ts +++ b/tests/integration/bff-express/tests/index.test.ts @@ -20,6 +20,7 @@ describe('bff express in dev', () => { const SSR_PAGE = 'ssr'; const BASE_PAGE = 'base'; const CUSTOM_PAGE = 'custom-sdk'; + const UPLOAD_PAGE = 'upload'; const host = `http://localhost`; const prefix = '/bff-api'; let app: any; @@ -71,6 +72,13 @@ describe('bff express in dev', () => { expect(text).toBe('Hello Custom SDK'); }); + test('support uoload', async () => { + await page.goto(`${host}:${port}/${UPLOAD_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.mock_file', el => el?.textContent); + expect(text).toBe('mock_image.png'); + }); + afterAll(async () => { await killApp(app); await page.close(); @@ -83,6 +91,7 @@ describe('bff express in prod', () => { const SSR_PAGE = 'ssr'; const BASE_PAGE = 'base'; const CUSTOM_PAGE = 'custom-sdk'; + const UPLOAD_PAGE = 'upload'; const host = `http://localhost`; const prefix = '/bff-api'; let app: any; @@ -139,6 +148,13 @@ describe('bff express in prod', () => { expect(text).toBe('Hello Custom SDK'); }); + test('support uoload', async () => { + await page.goto(`${host}:${port}/${UPLOAD_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.mock_file', el => el?.textContent); + expect(text).toBe('mock_image.png'); + }); + afterAll(async () => { await killApp(app); await page.close(); diff --git a/tests/integration/bff-koa/api/upload.ts b/tests/integration/bff-koa/api/upload.ts new file mode 100644 index 000000000000..ce6cb9c0c5e4 --- /dev/null +++ b/tests/integration/bff-koa/api/upload.ts @@ -0,0 +1,19 @@ +import { Api, Upload } from '@modern-js/runtime/koa'; +import { z } from 'zod'; + +const FileSchema = z.object({ + images: z.record(z.string(), z.any()), +}); + +export const upload = Api( + Upload('/upload', FileSchema), + async ({ formData }) => { + // do somethings + return { + data: { + code: 0, + file_name: formData.images.name, + }, + }; + }, +); diff --git a/tests/integration/bff-koa/src/modern-app-env.d.ts b/tests/integration/bff-koa/src/modern-app-env.d.ts index 76a15f489d6e..f95dac6dd24f 100644 --- a/tests/integration/bff-koa/src/modern-app-env.d.ts +++ b/tests/integration/bff-koa/src/modern-app-env.d.ts @@ -1,2 +1,3 @@ /// +/// /// diff --git a/tests/integration/bff-koa/src/routes/upload/page.tsx b/tests/integration/bff-koa/src/routes/upload/page.tsx new file mode 100644 index 000000000000..383a3e59be17 --- /dev/null +++ b/tests/integration/bff-koa/src/routes/upload/page.tsx @@ -0,0 +1,72 @@ +import { upload } from '@api/upload'; +import React, { useEffect } from 'react'; + +const getMockImage = () => { + const imageData = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='; + const blob = new Blob( + [Uint8Array.from(atob(imageData.split(',')[1]), c => c.charCodeAt(0))], + { type: 'image/png' }, + ); + + return new File([blob], 'mock_image.png', { type: 'image/png' }); +}; + +export default (): JSX.Element => { + const [file, setFile] = React.useState(); + const [fileName, setFileName] = React.useState(''); + + const handleChange = (e: React.ChangeEvent) => { + setFile(e.target.files); + }; + + const handleSubmit = async (e: React.FormEvent) => { + 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, + }); + } + }; + + const handleUpload = () => { + if (!file) { + return; + } + upload({ + files: { + images: file, + }, + }); + }; + + useEffect(() => { + upload({ + files: { + images: getMockImage(), + }, + }).then((res: any) => { + setFileName(res.data.file_name); + }); + }, []); + + return ( + <> +

File Upload

+

fileName: {fileName}

+
+ + +
+
+ + +
+ + ); +}; diff --git a/tests/integration/bff-koa/tests/index.test.ts b/tests/integration/bff-koa/tests/index.test.ts index cc2f2a84a001..faea750a20e4 100644 --- a/tests/integration/bff-koa/tests/index.test.ts +++ b/tests/integration/bff-koa/tests/index.test.ts @@ -19,6 +19,7 @@ if (isVersionAtLeast1819()) { describe('bff koa in dev', () => { let port = 8080; const host = `http://localhost`; + const UPLOAD_PAGE = 'upload'; let app: any; let page: Page; let browser: Browser; @@ -64,6 +65,13 @@ if (isVersionAtLeast1819()) { } }); + test('support uoload', async () => { + await page.goto(`${host}:${port}/${UPLOAD_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.mock_file', el => el?.textContent); + expect(text).toBe('mock_image.png'); + }); + afterAll(async () => { await killApp(app); await page.close(); @@ -74,6 +82,7 @@ if (isVersionAtLeast1819()) { describe('bff koa in prod', () => { let port = 8080; const host = `http://localhost`; + const UPLOAD_PAGE = 'upload'; let app: any; let page: Page; let browser: Browser; @@ -116,6 +125,13 @@ if (isVersionAtLeast1819()) { }); }); + test('support uoload', async () => { + await page.goto(`${host}:${port}/${UPLOAD_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.mock_file', el => el?.textContent); + expect(text).toBe('mock_image.png'); + }); + afterAll(async () => { await killApp(app); await page.close();