Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(uploads): Create uploads package with prisma extension and upload processors #11263

Merged
merged 104 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from 98 commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
11b234e
Create uploads package with extension
dac09 Aug 5, 2024
2d748b2
Fix types after generating local prisma client for testing
dac09 Aug 5, 2024
a32a3c8
Add gitignore
dac09 Aug 5, 2024
e08016d
Rename test schema
dac09 Aug 5, 2024
56f008a
Add initial tests
dac09 Aug 5, 2024
7133768
Update gitignore again
dac09 Aug 5, 2024
7d157ae
Add test for empty values
dac09 Aug 5, 2024
40f0011
Add prisma setup script
dac09 Aug 5, 2024
59f83cc
Update tsconfig
dac09 Aug 5, 2024
2c28d76
Update readme
dac09 Aug 5, 2024
bdbdccf
Add setup:test to watch command too
dac09 Aug 5, 2024
9ab71a7
Change types again
dac09 Aug 6, 2024
d8a6795
Fix types appearing correctly
dac09 Aug 6, 2024
8ddf5d6
Try using zx to setup
dac09 Aug 6, 2024
a0069db
Try cleaning prisma first
dac09 Aug 6, 2024
c2a268e
Remove publicUri extension for now. Add more tests, unhardcode withDa…
dac09 Aug 6, 2024
19c85a4
Get type mapping nearly there!
dac09 Aug 7, 2024
75bb556
Merge branch 'main' into feat/rw-uploads
dac09 Aug 7, 2024
5a884e5
Hack the result from compute
dac09 Aug 7, 2024
a7d06f4
Update comments
dac09 Aug 7, 2024
58c3530
Merge branch 'feat/rw-uploads' of github.com:dac09/redwood into feat/…
dac09 Aug 7, 2024
6974d45
Bump prisma client to match main
dac09 Aug 7, 2024
f5c696a
Fix extension types without prisma generate
dac09 Aug 7, 2024
fd28dd2
Return to generics
dac09 Aug 7, 2024
1a42840
Remove prisma test from CI, fix withDataUri
dac09 Aug 7, 2024
6dcf81f
Cleanup tests
dac09 Aug 7, 2024
60828f3
Try generating a local prisma client for tests
dac09 Aug 7, 2024
c5f462f
Remove extraneous beforeAll
dac09 Aug 7, 2024
f2571a7
Put . in front of schema :(
dac09 Aug 7, 2024
91c4601
Use db push instead
dac09 Aug 7, 2024
e5048a5
Add TUS tests
dac09 Aug 7, 2024
28041a4
Add changeset
dac09 Aug 7, 2024
087ed7f
Clean up in progress stuff
dac09 Aug 7, 2024
c6ec5d8
Remove old comment
dac09 Aug 7, 2024
ec85ddc
Fix weird CJS/ESM issue with JSON import
dac09 Aug 7, 2024
821c02a
Get types working for result extension
dac09 Aug 8, 2024
30cc64e
Clean up types and comments for result extends
dac09 Aug 8, 2024
734da00
Merge branch 'main' of github.com:redwoodjs/redwood into feat/rw-uploads
dac09 Aug 12, 2024
e80e556
WIP: adapter
dac09 Aug 13, 2024
71160fe
WIP: commit changes
dac09 Aug 14, 2024
7c74eaa
Merge branch 'main' of github.com:redwoodjs/redwood into feat/rw-uploads
dac09 Aug 15, 2024
b2dfc18
Regenerate lock file
dac09 Aug 15, 2024
3001a5e
Create processors, add some tests
dac09 Aug 15, 2024
4e6f56b
Remove outdated tests
dac09 Aug 15, 2024
808a023
Try setting prisma override
dac09 Aug 16, 2024
1c99b14
Add file list processor
dac09 Aug 16, 2024
9644339
WIP
dac09 Aug 16, 2024
52c57c9
Bit of cleanup
dac09 Aug 16, 2024
540bdcc
WIP: Start on signature generation
dac09 Aug 19, 2024
ca5cb34
Merge branch 'main' of github.com:redwoodjs/redwood into feat/rw-uplo…
dac09 Aug 19, 2024
d70ae4b
Uplaod yarn.lock
dac09 Aug 19, 2024
7478dcb
Setup signature generation and validation (no URLs just yet)
dac09 Aug 19, 2024
6243643
Update extension tests
dac09 Aug 19, 2024
a5fd94a
Move filename generation to base adapter
dac09 Aug 19, 2024
f1aaafb
More signed url tests
dac09 Aug 19, 2024
e48d364
Few changes
dac09 Aug 19, 2024
75b3835
Add withSignedUrl result extension
dac09 Aug 19, 2024
ef8aa4a
Get UploadConfig type working with fields
dac09 Aug 19, 2024
a751337
Fix types for resultExtension
dac09 Aug 19, 2024
8cd0081
Keep these changes
dac09 Aug 19, 2024
32942a4
Warn comment on vitest alias
dac09 Aug 19, 2024
32e870d
Fix typing on keys being hardcoded
dac09 Aug 19, 2024
4274d95
Refactor signed url generator
dac09 Aug 20, 2024
db9522d
Merge branch 'main' into feat/rw-uploads-extension
dac09 Aug 20, 2024
33926d6
Update tests too
dac09 Aug 20, 2024
4cebfd1
Pass urlSigner instance to setup
dac09 Aug 20, 2024
d2b57f7
Refactor exports, etc.
dac09 Aug 20, 2024
2119b5d
Fix signedurl creation
dac09 Aug 20, 2024
cb08dc2
Fix signing by using s instead of signature as argument
dac09 Aug 20, 2024
c6d2b09
Don't throw when invalid upload path passed to delete
dac09 Aug 21, 2024
a0634c1
Handle empty lists in file list processor
dac09 Aug 21, 2024
06307f3
Use storage adapter in dataUri
dac09 Aug 21, 2024
3d16151
Merge branch 'main' into feat/rw-uploads-extension
dac09 Aug 22, 2024
7d5776e
Merge branch 'feat/rw-uploads-extension' of github.com:dac09/redwood …
dac09 Aug 22, 2024
d2d207f
Constraints
dac09 Aug 22, 2024
0ef44e3
Refactor base64 handling
dac09 Aug 22, 2024
cd43150
Update README
dac09 Aug 22, 2024
3b8d4a1
Handle update edgecases
dac09 Aug 22, 2024
9fc42ce
Prettify
dac09 Aug 22, 2024
e400acf
Add Memory storage tests
dac09 Aug 22, 2024
2f7d822
Add tests for storage adapters
dac09 Aug 22, 2024
1d94e1f
Remove import aliasing for now
dac09 Aug 22, 2024
5bc4ae4
Clean up package.json
dac09 Aug 22, 2024
874b09a
Merge branch 'main' into feat/rw-uploads-extension
dac09 Aug 23, 2024
33ee3bb
Build prisma client before building package
dac09 Aug 23, 2024
d4f0d44
Ignore generated prisma client from linting
dac09 Aug 23, 2024
7eaa1b0
Remove only from tests
dac09 Aug 23, 2024
0e74764
Update tests to pass on windows too
dac09 Aug 23, 2024
e0115a2
Missed one test
dac09 Aug 23, 2024
17994be
Merge branch 'main' into feat/rw-uploads-extension
dac09 Aug 26, 2024
4cbb3b1
Add new line to end of package.json 🤷
dac09 Aug 26, 2024
e050b83
Merge branch 'feat/rw-uploads-extension' of github.com:dac09/redwood …
dac09 Aug 26, 2024
6a297d4
Put fileList processor inside processors
dac09 Aug 26, 2024
9b474d7
Remove outdated TODOs
dac09 Aug 26, 2024
b7f6b9f
Prettier
dac09 Aug 26, 2024
3434411
Make with signedUrl result extension take an object instead
dac09 Aug 27, 2024
a0d4183
Make SignedUrl args optional
dac09 Aug 28, 2024
e002a7a
Merge branch 'main' into feat/rw-uploads-extension
dac09 Aug 29, 2024
6d683c7
Accept dataloss in uploads prisma setup for tests
dac09 Aug 29, 2024
1b43a23
Add tests for delete failure
dac09 Aug 29, 2024
ef25555
Merge branch 'main' into feat/rw-uploads-extension
dac09 Aug 29, 2024
b6586ef
Whoopsie
dac09 Aug 29, 2024
4508df7
Allow empty catch in query extension test
dac09 Aug 29, 2024
5351342
Lint
dac09 Aug 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading