Skip to content

Commit

Permalink
feat(uploads): Create uploads package with prisma extension and uploa…
Browse files Browse the repository at this point in the history
…d processors (#11263)
  • Loading branch information
dac09 authored Aug 29, 2024
1 parent 1719fd5 commit aba457a
Show file tree
Hide file tree
Showing 28 changed files with 2,054 additions and 0 deletions.
124 changes: 124 additions & 0 deletions .changesets/11154.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
- feat(rw-uploads): Create uploads package with prisma extension and upload processor (#11154) by @dac09

Introduces `@redwoodjs/uploads` package which houses

- Prisma extension for handling uploads. Currently
a) Query Extension: will save, delete, replace files on disk during CRUD
b) Result Extension: gives you functions like `.withSignedUri` on configured prisma results - which will take the paths, and convert it to a signed url
- Storage adapters e.g. FS and Memory to use with the prisma extension
- Processors - i.e. utility functions which will take [`Files`](https://developer.mozilla.org/en-US/docs/Web/API/File) and save them to storage

## Usage

In `api/src/uploads.ts` - setup uploads - processors, storage and the prisma extension.

```ts
// api/src/lib/uploads.ts

import { UploadsConfig } from '@redwoodjs/uploads'
import { setupUploads } from '@redwoodjs/uploads'
import { FileSystemStorage } from '@redwoodjs/uploads/FileSystemStorage'
import { UrlSigner } from '@redwoodjs/uploads/signedUrl'

const uploadConfig: UploadsConfig = {
// 👇 prisma model
profile: {
// 👇 pass in fields that are going to be File uploads
// these should be configured as string in the Prisma.schema
fields: ['avatar', 'coverPhoto'],
},
}

// 👇 exporting these allows you access elsewhere on the api side
export const storage = new FileSystemStorage({
baseDir: './uploads',
})

// Optional
export const urlSigner = new UrlSigner({
secret: process.env.UPLOADS_SECRET,
endpoint: '/signedUrl',
})

const { uploadsProcessors, prismaExtension, fileListProcessor } = setupUploads(
uploadConfig,
storage,
urlSigner,
)

export { uploadsProcessors, prismaExtension, fileListProcessor }
```

### Configuring db to use the prisma extension

```ts
// api/src/lib/db.ts

import { PrismaClient } from '@prisma/client'

import { emitLogLevels, handlePrismaLogging } from '@redwoodjs/api/logger'

import { logger } from './logger'
import { prismaExtension } from './uploads'

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

handlePrismaLogging({
db: prismaClient,
logger,
logLevels: ['info', 'warn', 'error'],
})

// 👇 Export db after adding uploads extension
export const db = prismaClient.$extends(prismaExtension)
```

## Using Prisma extension

### A) CRUD operations

No need to do anything here, but you have to use processors to supply Prisma with data in the correct format.

### B) Result extensions

```ts
// api/src/services/profiles/profiles.ts

export const profile: QueryResolvers['profile'] = async ({ id }) => {
// 👇 await the result from your prisma query
const profile = await db.profile.findUnique({
where: { id },
})

// Convert the avatar and coverPhoto fields to signed URLs
// Note that you still need to add a api endpoint to handle these signed urls
return profile?.withSignedUrl()
}
```

## Using processors

In your services, you can use the preconfigured "processors" to convert Files to strings for Prisma to save into the database. The processors, and storage adapters determine where the file is saved.

```ts
// api/src/services/profiles/profiles.ts

export const updateProfile: MutationResolvers['updateProfile'] = async ({
id,
input,
}) => {
const processedInput = await uploadsProcessors.processProfileUploads(input)

// This becomes a string 👇
// The configuration on where it was saved is passed when we setup uploads in src/lib/uploads.ts
// processedInput.avatar = '/mySavePath/profile/avatar/generatedId.jpg'

return db.profile.update({
data: processedInput,
where: { id },
})
}
```
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ module.exports = {
'packages/babel-config/src/__tests__/__fixtures__/**/*',
'packages/codemods/**/__testfixtures__/**/*',
'packages/cli/**/__testfixtures__/**/*',
'packages/uploads/src/__tests__/prisma-client/*',
],
rules: {
curly: 'error',
Expand Down
4 changes: 4 additions & 0 deletions packages/uploads/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
src/__tests__/migrations/*
src/__tests__/for_unit_test.db*
.attw.json
src/__tests__/prisma-client/*
124 changes: 124 additions & 0 deletions packages/uploads/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# `@redwoodjs/uploads`

This package houses

- Prisma extension for handling uploads. Currently
a) Query Extension: will save, delete, replace files on disk during CRUD
b) Result Extension: gives you functions like `.withSignedUri` on configured prisma results - which will take the paths, and convert it to a signed url
- Storage adapters e.g. FS and Memory to use with the prisma extension
- Processors - i.e. utility functions which will take [`Files`](https://developer.mozilla.org/en-US/docs/Web/API/File) and save them to storage

## Usage

In `api/src/uploads.ts` - setup uploads - processors, storage and the prisma extension.

```ts
// api/src/lib/uploads.ts

import { UploadsConfig } from '@redwoodjs/uploads'
import { setupUploads } from '@redwoodjs/uploads'
import { FileSystemStorage } from '@redwoodjs/uploads/FileSystemStorage'
import { UrlSigner } from '@redwoodjs/uploads/signedUrl'

const uploadConfig: UploadsConfig = {
// 👇 prisma model
profile: {
// 👇 pass in fields that are going to be File uploads
// these should be configured as string in the Prisma.schema
fields: ['avatar', 'coverPhoto'],
},
}

// 👇 exporting these allows you access elsewhere on the api side
export const storage = new FileSystemStorage({
baseDir: './uploads',
})

// Optional
export const urlSigner = new UrlSigner({
secret: process.env.UPLOADS_SECRET,
endpoint: '/signedUrl',
})

const { uploadsProcessors, prismaExtension, fileListProcessor } = setupUploads(
uploadConfig,
storage,
urlSigner,
)

export { uploadsProcessors, prismaExtension, fileListProcessor }
```

### Configuring db to use the prisma extension

```ts
// api/src/lib/db.ts

import { PrismaClient } from '@prisma/client'

import { emitLogLevels, handlePrismaLogging } from '@redwoodjs/api/logger'

import { logger } from './logger'
import { prismaExtension } from './uploads'

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

handlePrismaLogging({
db: prismaClient,
logger,
logLevels: ['info', 'warn', 'error'],
})

// 👇 Export db after adding uploads extension
export const db = prismaClient.$extends(prismaExtension)
```

## Using Prisma extension

### A) CRUD operations

No need to do anything here, but you have to use processors to supply Prisma with data in the correct format.

### B) Result extensions

```ts
// api/src/services/profiles/profiles.ts

export const profile: QueryResolvers['profile'] = async ({ id }) => {
// 👇 await the result from your prisma query
const profile = await db.profile.findUnique({
where: { id },
})

// Convert the avatar and coverPhoto fields to signed URLs
// Note that you still need to add a api endpoint to handle these signed urls
return profile?.withSignedUrl()
}
```

## Using processors

In your services, you can use the preconfigured "processors" to convert Files to strings for Prisma to save into the database. The processors, and storage adapters determine where the file is saved.

```ts
// api/src/services/profiles/profiles.ts

export const updateProfile: MutationResolvers['updateProfile'] = async ({
id,
input,
}) => {
const processedInput = await uploadsProcessors.processProfileUploads(input)

// This becomes a string 👇
// The configuration on where it was saved is passed when we setup uploads in src/lib/uploads.ts
// processedInput.avatar = '/mySavePath/profile/avatar/generatedId.jpg'

return db.profile.update({
data: processedInput,
where: { id },
})
}
```
31 changes: 31 additions & 0 deletions packages/uploads/attw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { $ } from 'zx'

interface Problem {
kind: string
entrypoint?: string
resolutionKind?: string
}

await $({ nothrow: true })`yarn attw -P -f json > .attw.json`
const output = await $`cat .attw.json`
await $`rm .attw.json`

const json = JSON.parse(output.stdout)

if (!json.analysis.problems || json.analysis.problems.length === 0) {
console.log('No errors found')
process.exit(0)
}

if (
json.analysis.problems.every(
(problem: Problem) => problem.resolutionKind === 'node10',
)
) {
console.log("Only found node10 problems, which we don't care about")
process.exit(0)
}

console.log('Errors found')
console.log(json.analysis.problems)
process.exit(1)
33 changes: 33 additions & 0 deletions packages/uploads/build.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { build, defaultBuildOptions } from '@redwoodjs/framework-tools'
import {
generateTypesCjs,
generateTypesEsm,
insertCommonJsPackageJson,
} from '@redwoodjs/framework-tools/generateTypes'

// ESM build
await build({
buildOptions: {
...defaultBuildOptions,
format: 'esm',
packages: 'external',
},
})

await generateTypesEsm()

// CJS build
await build({
buildOptions: {
...defaultBuildOptions,
outdir: 'dist/cjs',
packages: 'external',
},
})

await generateTypesCjs()

await insertCommonJsPackageJson({
buildFileUrl: import.meta.url,
cjsDir: 'dist/cjs',
})
Loading

0 comments on commit aba457a

Please sign in to comment.