Skip to content

Commit

Permalink
Customize request sent to API route (#73)
Browse files Browse the repository at this point in the history
Send extra info with requests so the key function can use user data to customize the path
  • Loading branch information
ryanto authored Apr 29, 2022
1 parent af51b86 commit 27531f5
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// <reference types="cypress" />

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");
});
});
16 changes: 16 additions & 0 deletions packages/docs-site/src/pages/api/custom-key-based-on-request.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
});
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col space-y-4">
<div>
<input
type="text"
value={headerName}
onChange={e => setHeaderName(e.target.value)}
data-test="header-name-input"
/>
</div>

<div>
<input
type="text"
value={bodyName}
onChange={e => setBodyName(e.target.value)}
data-test="body-name-input"
/>
</div>

<div>
<input
type="file"
name="file"
multiple={true}
data-test="file-input"
onChange={handleFileChange}
/>
</div>

{imageUrl && (
<div className="flex space-x-6">
<div className="w-1/2">
<img className="object-contain" src={imageUrl} data-test="image" />
</div>
<div className="w-1/2">
<div className="mt-2 text-sm text-gray-500">URL:</div>
<div data-test="url">{imageUrl}</div>
</div>
</div>
)}
</div>
);
}
91 changes: 88 additions & 3 deletions packages/docs-site/src/pages/s3-file-paths.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>`.
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}`;
}
});
```
22 changes: 21 additions & 1 deletion packages/docs-site/src/pages/use-s3-upload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 40 additions & 3 deletions packages/next-s3-upload/src/hooks/use-s3-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,23 @@ type UploadResult = {
key: string;
};

type UploadToS3 = (file: File) => Promise<UploadResult>;
type RequestOptions = {
body: Record<string, any>;
headers: HeadersInit;
};

type EndpointOptions = {
request: RequestOptions;
};

type UploadToS3Options = {
endpoint?: EndpointOptions;
};

type UploadToS3 = (
file: File,
options?: UploadToS3Options
) => Promise<UploadResult>;

type UseS3UploadTools = {
FileInput: (props: any) => ReactElement<HTMLInputElement>;
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/next-s3-upload/src/pages/api/s3-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '-')}`;
Expand Down

1 comment on commit 27531f5

@vercel
Copy link

@vercel vercel bot commented on 27531f5 Apr 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.