From 27531f52b8f78ea14f7ca72ec801ab5ec270f912 Mon Sep 17 00:00:00 2001 From: Ryan Toronto Date: Thu, 28 Apr 2022 22:43:02 -0400 Subject: [PATCH] Customize request sent to API route (#73) Send extra info with requests so the key function can use user data to customize the path --- .../custom-key-based-on-request.spec.js | 16 ++++ .../pages/api/custom-key-based-on-request.js | 16 ++++ .../examples/custom-key-based-on-request.js | 75 +++++++++++++++ .../docs-site/src/pages/s3-file-paths.mdx | 91 ++++++++++++++++++- .../docs-site/src/pages/use-s3-upload.mdx | 22 ++++- .../src/hooks/use-s3-upload.tsx | 43 ++++++++- .../next-s3-upload/src/pages/api/s3-upload.ts | 2 +- 7 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 packages/docs-site/cypress/integration/custom-key-based-on-request.spec.js create mode 100644 packages/docs-site/src/pages/api/custom-key-based-on-request.js create mode 100644 packages/docs-site/src/pages/examples/custom-key-based-on-request.js diff --git a/packages/docs-site/cypress/integration/custom-key-based-on-request.spec.js b/packages/docs-site/cypress/integration/custom-key-based-on-request.spec.js new file mode 100644 index 0000000..89f004f --- /dev/null +++ b/packages/docs-site/cypress/integration/custom-key-based-on-request.spec.js @@ -0,0 +1,16 @@ +/// + +describe("Custom key paths based on requests", () => { + it("should upload to S3 using a custom key path based on the request", () => { + cy.visit("/examples/custom-key-based-on-request"); + + cy.get("[data-test=header-name-input]").type("header"); + cy.get("[data-test=body-name-input]").type("body"); + cy.get("[data-test=file-input]").attachFile("woods.jpg"); + + cy.get("[data-test=image]").isFixtureImage("woods.jpg"); + cy.get("[data-test=url]") + .contains("/header/body/woods.jpg") + .should("exist"); + }); +}); diff --git a/packages/docs-site/src/pages/api/custom-key-based-on-request.js b/packages/docs-site/src/pages/api/custom-key-based-on-request.js new file mode 100644 index 0000000..d77b7cc --- /dev/null +++ b/packages/docs-site/src/pages/api/custom-key-based-on-request.js @@ -0,0 +1,16 @@ +import { APIRoute } from "next-s3-upload"; + +export default APIRoute.configure({ + async key(req, filename) { + return new Promise(resolve => { + let bodyName = req.body.bodyName; + let headerName = req.headers["x-header-name"]; + + let path = `${headerName}/${bodyName}/${filename}`; + + setTimeout(() => { + resolve(path); + }, 1000); + }); + } +}); diff --git a/packages/docs-site/src/pages/examples/custom-key-based-on-request.js b/packages/docs-site/src/pages/examples/custom-key-based-on-request.js new file mode 100644 index 0000000..d22b556 --- /dev/null +++ b/packages/docs-site/src/pages/examples/custom-key-based-on-request.js @@ -0,0 +1,75 @@ +import { useState } from "react"; +import { useS3Upload } from "next-s3-upload"; + +export default function UploadTest() { + let [headerName, setHeaderName] = useState(""); + let [bodyName, setBodyName] = useState(""); + let [imageUrl, setImageUrl] = useState(); + + let { uploadToS3 } = useS3Upload({ + endpoint: "/api/custom-key-based-on-request" + }); + + const handleFileChange = async ({ target }) => { + let file = target.files[0]; + + let { url } = await uploadToS3(file, { + endpoint: { + request: { + headers: { + "X-Header-Name": headerName + }, + body: { + bodyName + } + } + } + }); + + setImageUrl(url); + }; + + return ( +
+
+ setHeaderName(e.target.value)} + data-test="header-name-input" + /> +
+ +
+ setBodyName(e.target.value)} + data-test="body-name-input" + /> +
+ +
+ +
+ + {imageUrl && ( +
+
+ +
+
+
URL:
+
{imageUrl}
+
+
+ )} +
+ ); +} diff --git a/packages/docs-site/src/pages/s3-file-paths.mdx b/packages/docs-site/src/pages/s3-file-paths.mdx index 6d8865a..2748e23 100644 --- a/packages/docs-site/src/pages/s3-file-paths.mdx +++ b/packages/docs-site/src/pages/s3-file-paths.mdx @@ -21,10 +21,95 @@ import { APIRoute } from "next-s3-upload"; export default APIRoute.configure({ async key(req, filename) { - const user = await currentUser(req); - return `users/${user.id}/${filename}`; + let path = await getPath(); + return `${path}/${filename}`; + } +}); +``` + +## Data from React + +You can pass data from your React app to the key function using `uploadToS3` options. + +In the example below, we pass a `projectId` from the frontend using the `endpoint.request.body` option. + +```js +// Frontend component +function Component() { + let { uploadToS3 } = useS3Upload(); + + let handleSubmit = async () => { + // You can pass extra data using `endpoint.request.body` + + await uploadToS3(file, { + endpoint: { + request: { + body: { + projectId: 123 + } + } + } + }); + }; +} +``` + +Now the key function can read the passed `projectId` from `req.body.projectId`. + +```js +// pages/api/s3-upload.js +import { APIRoute } from "next-s3-upload"; + +export default APIRoute.configure({ + async key(req, filename) { + let projectId = req.body.projectId; // 123 + + return `projects/${projectId}/${filename}`; } }); ``` -The signature of the key function is: `(req: NextApiRequest, filename: string) => string | Promise`. +All data that needs to be passed from the frontend should be sent under the `endpoint.request.body` object, since that data will get serialized into the request's body. + +You can pass any data you'd like here, as long as it's serializable into JSON. + +## Headers + +The `uploadToS3` function can also add request headers that can be used by the key function. + +```js +// Frontend component +function Component() { + let { user } = useAuth(); + let { uploadToS3 } = useS3Upload(); + + let handleSubmit = async () => { + let authToken = await user.getAuthToken(); + + await uploadToS3(file, { + endpoint: { + request: { + headers: { + authorization: authToken + } + } + } + }); + }; +} +``` + +And the authorization header can be read using `req.headers.authorization`. + +```js +// pages/api/s3-upload.js +import { APIRoute } from "next-s3-upload"; + +export default APIRoute.configure({ + async key(req, filename) { + let user = await getUserFromAuthToken(req.headers.authorization); + + return `users/${user.id}/${filename}`; + } +}); +``` diff --git a/packages/docs-site/src/pages/use-s3-upload.mdx b/packages/docs-site/src/pages/use-s3-upload.mdx index ea649e7..a94704a 100644 --- a/packages/docs-site/src/pages/use-s3-upload.mdx +++ b/packages/docs-site/src/pages/use-s3-upload.mdx @@ -14,9 +14,29 @@ const Component = () => { | ------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | `FileInput` | A component that renders a hidden file input. It needs to be rendered on the page in order to coordinate file access. | | `openFileDialog` | A function that opens the browser's select a file dialog. Once a file is selected the `FileInput`'s `onChange` action will fire. | -| `uploadToS3(file)` | A function that will upload a file from a file input to your S3 bucket. | +| `uploadToS3(file)` | A function that will upload a file from a file input to your S3 bucket. For details on options, see uploadToS3 options below. | | `files` | Any array of files objects, see `Files` below. | +## uploadToS3 Options + +The `uploadToS3` can take options that allow you to customize the request send to your API route. + +```js +uploadToS3(file, { + endpoint: { + request: { + body: {}, + headers: {} + } + } +}); +``` + +| Option | Description | +| -------------------------- | ----------------------------------------- | +| `endpoint.request.body` | Additional data sent in the body payload. | +| `endpoint.request.headers` | Additional HTTP request headers. | + ## Files The `files` array returned from `useS3Upload()` contains a list of files that are currently uploading or have already been uploaded. Each object in the list has the following structure. diff --git a/packages/next-s3-upload/src/hooks/use-s3-upload.tsx b/packages/next-s3-upload/src/hooks/use-s3-upload.tsx index 080ecc5..cf99bc1 100644 --- a/packages/next-s3-upload/src/hooks/use-s3-upload.tsx +++ b/packages/next-s3-upload/src/hooks/use-s3-upload.tsx @@ -46,7 +46,23 @@ type UploadResult = { key: string; }; -type UploadToS3 = (file: File) => Promise; +type RequestOptions = { + body: Record; + headers: HeadersInit; +}; + +type EndpointOptions = { + request: RequestOptions; +}; + +type UploadToS3Options = { + endpoint?: EndpointOptions; +}; + +type UploadToS3 = ( + file: File, + options?: UploadToS3Options +) => Promise; type UseS3UploadTools = { FileInput: (props: any) => ReactElement; @@ -70,9 +86,30 @@ export const useS3Upload: UseS3Upload = (options = {}) => { let endpoint = options.endpoint ?? '/api/s3-upload'; - let uploadToS3: UploadToS3 = async file => { + let uploadToS3: UploadToS3 = async (file, options = {}) => { let filename = encodeURIComponent(file.name); - let res = await fetch(`${endpoint}?filename=${filename}`); + + let requestExtras = options?.endpoint?.request ?? { + headers: {}, + body: {}, + }; + + let body = { + filename, + ...requestExtras.body, + }; + + let headers = { + ...requestExtras.headers, + 'Content-Type': 'application/json', + }; + + let res = await fetch(endpoint, { + method: 'POST', + headers, + body: JSON.stringify(body), + }); + let data = await res.json(); if (data.error) { diff --git a/packages/next-s3-upload/src/pages/api/s3-upload.ts b/packages/next-s3-upload/src/pages/api/s3-upload.ts index b9f5658..dd75c07 100644 --- a/packages/next-s3-upload/src/pages/api/s3-upload.ts +++ b/packages/next-s3-upload/src/pages/api/s3-upload.ts @@ -30,7 +30,7 @@ let makeRouteHandler = (options: Options = {}): Handler => { let bucket = process.env.S3_UPLOAD_BUCKET; - let filename = req.query.filename as string; + let filename = req.body.filename; let key = options.key ? await Promise.resolve(options.key(req, filename)) : `next-s3-uploads/${uuidv4()}/${filename.replace(/\s/g, '-')}`;