Skip to content

Commit

Permalink
docs(uploads): Tweaks from updating the Recipes example app (#11571)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe authored Sep 16, 2024
1 parent 187bc70 commit 7173844
Showing 1 changed file with 46 additions and 38 deletions.
84 changes: 46 additions & 38 deletions docs/docs/uploads.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ You're now ready to receive files!

### 2. Configuring the UI

Let's setup a basic form to add avatar images to your profile.

Assuming you've built a [Form](forms.md) for profile
Assuming you've built a [Form](forms.md) for your profile let's add a
`FileField` to it.

```tsx title="web/src/components/ProfileForm.tsx"
// highlight-next-line
Expand All @@ -44,18 +43,18 @@ import { FileField, TextField, FieldError } from '@redwoodjs/forms'
export const ProfileForm = ({ onSubmit }) => {
return {
<Form onSubmit={onSubmit}>
<div>
<Label name="firstName" /*...*/ >
First name
</Label>
<TextField name="firstName" /*...*/ />
<FieldError name="firstName" />
<Label name="lastName" /*...*/ >
Last name
</Label>
<TextField name="lastName" /*...*/ />
<FieldError name="lastName" />
</div>
<Label name="firstName" /*...*/ >
First name
</Label>
<TextField name="firstName" /*...*/ />
<FieldError name="firstName" />

<Label name="lastName" /*...*/ >
Last name
</Label>
<TextField name="lastName" /*...*/ />
<FieldError name="lastName" />

// highlight-next-line
<FileField name="avatar" /*...*/ />
</Form>
Expand All @@ -71,13 +70,13 @@ Now we need to send the file as a mutation!
import { useMutation } from '@redwoodjs/web'

const UPDATE_PROFILE_MUTATION = gql`
// This is the Input type we setup with File earlier!
// highlight-next-line
// This is the Input type we setup with File earlier!
// highlight-next-line
mutation UpdateProfileMutation($input: UpdateProfileInput!) {
updateProfile(input: $input) {
firstName
lastName
// highlight-next-line
// highlight-next-line
avatar
}
}
Expand All @@ -96,8 +95,10 @@ const EditProfile = ({ profile }) => {

const input = {
...formData,
// FileField returns an array, we want the first and only file; Multi-file
// uploads are available
// highlight-next-line
avatar: formData.avatar?.[0], // FileField returns an array, we want the first and only file; Multi-file uploads are available
avatar: formData.avatar?.[0],
}

updateProfile({ variables: { input } })
Expand All @@ -120,7 +121,7 @@ While [multi-file uploads are possible](#saving-file-lists---savefilesinlist), w

Try uploading your avatar photo now, and if you log the `avatar` field in your service:

```ts title="api/src/services/profile.ts"
```ts title="api/src/services/profiles/profiles.ts"
export const updateProfile = async ({ id, input }) => {
// highlight-next-line
console.log(input.avatar)
Expand Down Expand Up @@ -156,7 +157,7 @@ On the backend, GraphQL Yoga is pre-configured to handle multipart form requests

## Storage

Great, now you can receive Files from GraphQL - but how do you go about saving them, and tracking them, in your database? Well, Redwood has the answers for you! Keep going to find out how!
Great, now you can receive Files from GraphQL - but how do you go about saving them to disk, while also tracking them in your database? Well, Redwood has the answers for you! Keep going to find out how!

### 1. Configuring the Prisma schema

Expand All @@ -171,7 +172,7 @@ model Profile {
}
```

This is because Prisma doesn't have a native File type. Instead, we store the file path or URL as a string in the database. The actual file processing and storage will be handled in your service layer, and pass the path to Prisma to save.
This is because Prisma doesn't have a native File type. Instead, we store the file path or URL as a string in the database. The actual file processing and storage will be handled in your service layer, and then the path to the uploaded file is passed to Prisma to save.

### 2. Configuring the Upload savers and Uploads extension

Expand All @@ -191,9 +192,9 @@ yarn rw setup uploads

This will do three things:

1. Generate a configuration file in `api/src/lib/uploads.{ts,js}`
1. Generate a configuration file in `api/src/lib/uploads.{js,ts}`
2. Configure your Prisma client with the storage extension
3. Generate a signedUrl function
3. Generate a `signedUrl` function

Let's break down the key components of the configuration.

Expand Down Expand Up @@ -231,7 +232,7 @@ export { saveFiles, storagePrismaExtension }
```

**1. Upload Configuration**
This is where you configure the fields that will receive uploads. In our case, it's the profile.avatar field.
This is where you configure the fields that will receive uploads. In our case, it's the `profile.avatar` field.

The shape of the config looks like this:

Expand Down Expand Up @@ -259,11 +260,11 @@ We provide utility functions that can be exported from this file to be used else
saveFiles.forProfile(gqlInput)
```

- `storagePrismaExtension` - The Prisma client extension we'll use in `api/src/lib/db.ts` to automatically handle updates, deletion of uploaded files (including when the Prisma operation fails). It also configures [Result extensions](https://www.prisma.io/docs/orm/prisma-client/client-extensions/result), to give you utilities like `profile.withSignedUrl()`.
- `storagePrismaExtension` - The Prisma client extension we'll use in `api/src/lib/db.{js,ts}` to automatically handle updates, deletion of uploaded files (including when the Prisma operation fails). It also configures [Result extensions](https://www.prisma.io/docs/orm/prisma-client/client-extensions/result), to give you utilities like `profile.withSignedUrl()`.

### 3. Attaching the Uploads extension

Now we need to extend our db client in `api/src/lib/db.ts` to use the configured prisma client.
Now we need to extend our db client in `api/src/lib/db.{js,ts}` to use the configured prisma client.

```ts title="api/src/lib/db.ts"
import { PrismaClient } from '@prisma/client'
Expand All @@ -274,8 +275,8 @@ import { logger } from './logger'
// highlight-next-line
import { storagePrismaExtension } from './uploads'

// 👇 Notice here we create prisma client, and don't export it yet
export const prismaClient = new PrismaClient({
// 👇 Notice here we create prisma client, but don't export it yet
const prismaClient = new PrismaClient({
log: emitLogLevels(['info', 'warn', 'error']),
})

Expand All @@ -299,10 +300,11 @@ More details on these extensions can be found [here](#storage-prisma-extension).

<details>
<summary>
__Why Export This Way__
__Why Export This Way__
</summary>

The `$extends` method returns a new instance of the Prisma client with the extensions applied. By exporting this new instance as db, you ensure that any additional functionality provided by the uploads extension is available throughout your application, without needing to change where you import. Note one of the [limitations](https://www.prisma.io/docs/orm/prisma-client/client-extensions#limitations) of using extensions is you have to use `$on` on your prisma client (as we do in handlePrismaLogging), it needs to happen before you use `$extends`
The `$extends` method returns a new instance of the Prisma client with the extensions applied. By exporting this new instance as `db`, you ensure that any additional functionality provided by the uploads extension is available throughout your application, without needing to change where you import.
Note one of the [limitations](https://www.prisma.io/docs/orm/prisma-client/client-extensions#limitations) of using extensions is if you have to use `$on` on your prisma client (as we do in handlePrismaLogging), it needs to happen before you use `$extends`

</details>

Expand Down Expand Up @@ -361,7 +363,7 @@ You might have already noticed that the saver functions sort-of tie your GraphQL

In essence, these utility functions expect to take an object very similar to the Prisma data argument (the data you're passing to your `create`, `update`), but with File objects at fields `avatar`, and `document` instead of strings.

If your `File` is in a different key (or a key did you did not configure in the upload config), it will be ignored and left as-is.
If your `File` is in a different key (or a key you did not configure in the upload config), it will be ignored and left as-is.

:::

Expand Down Expand Up @@ -575,7 +577,7 @@ The extension is determined by the name of the uploaded file.

When you setup uploads, we also generate an API function (an endpoint) for you - by default at `/signedUrl`. You can use this in conjunction with the `.withSignedUrl` helper. For example:

```ts title="api/src/services/profiles.ts"
```ts title="api/src/services/profiles/profiles.ts"
import { EXPIRES_IN } from '@redwoodjs/storage/UrlSigner'

export const profile = async ({ id }) => {
Expand Down Expand Up @@ -770,17 +772,23 @@ main()

Based on the above, you'll be able to access your files at:

```
http://localhost:8910/.redwood/functions/public_uploads/01J6AF89Y89WTWZF12DRC72Q2A.jpeg
`http://localhost:8910/.redwood/functions/public_uploads/01J6AF89Y89WTWZF12DRC72Q2A.jpeg`

OR directly

http://localhost:8911/public_uploads/01J6AF89Y89WTWZF12DRC72Q2A.jpeg
```
`http://localhost:8911/public_uploads/01J6AF89Y89WTWZF12DRC72Q2A.jpeg`

Where you are only exposing **part** of your uploads directory publicly

In your web side code you can construct the URL like this:

```ts
const publicUrl = `${global.RWJS_API_URL}/${profile.avatar.replace(
'uploads/public_profile_photos/',
'public_uploads/'
)}`
```

### Customizing the body limit for requests

The default body size limit for the Redwood API server is 100MB (per request). Depending on the sizes of files you're uploading, especially in the case of multiple files, you may receive errors like this:
Expand Down

0 comments on commit 7173844

Please sign in to comment.