feat: storage-uploadthing package (#6316)
Co-authored-by: James <james@trbl.design>
This commit is contained in:
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -16,6 +16,14 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal"
|
"type": "node-terminal"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "node --no-deprecation test/dev.js storage-uploadthing",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"name": "Uploadthing",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal",
|
||||||
|
"envFile": "${workspaceFolder}/test/storage-uploadthing/.env"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "node --no-deprecation test/dev.js live-preview",
|
"command": "node --no-deprecation test/dev.js live-preview",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
|
|||||||
@@ -33,6 +33,11 @@
|
|||||||
"build:plugins": "turbo build --filter \"@payloadcms/plugin-*\"",
|
"build:plugins": "turbo build --filter \"@payloadcms/plugin-*\"",
|
||||||
"build:richtext-lexical": "turbo build --filter richtext-lexical",
|
"build:richtext-lexical": "turbo build --filter richtext-lexical",
|
||||||
"build:richtext-slate": "turbo build --filter richtext-slate",
|
"build:richtext-slate": "turbo build --filter richtext-slate",
|
||||||
|
"build:storage-azure": "turbo build --filter storage-azure",
|
||||||
|
"build:storage-gcs": "turbo build --filter storage-gcs",
|
||||||
|
"build:storage-s3": "turbo build --filter storage-s3",
|
||||||
|
"build:storage-uploadthing": "turbo build --filter storage-uploadthing",
|
||||||
|
"build:storage-vercel-blob": "turbo build --filter storage-vercel-blob",
|
||||||
"build:tests": "pnpm --filter payload-test-suite run typecheck",
|
"build:tests": "pnpm --filter payload-test-suite run typecheck",
|
||||||
"build:translations": "turbo build --filter translations",
|
"build:translations": "turbo build --filter translations",
|
||||||
"build:ui": "turbo build --filter ui",
|
"build:ui": "turbo build --filter ui",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Collection, PayloadRequestWithData, Where } from 'payload/types'
|
import type { Collection, PayloadRequestWithData, TypeWithID, Where } from 'payload/types'
|
||||||
|
|
||||||
import { executeAccess } from 'payload/auth'
|
import { executeAccess } from 'payload/auth'
|
||||||
import { Forbidden } from 'payload/errors'
|
import { Forbidden } from 'payload/errors'
|
||||||
@@ -13,7 +13,7 @@ export async function checkFileAccess({
|
|||||||
collection: Collection
|
collection: Collection
|
||||||
filename: string
|
filename: string
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
}) {
|
}): Promise<Response | TypeWithID> {
|
||||||
const { config } = collection
|
const { config } = collection
|
||||||
const disableEndpoints = endpointsAreDisabled({ endpoints: config.endpoints, request: req })
|
const disableEndpoints = endpointsAreDisabled({ endpoints: config.endpoints, request: req })
|
||||||
if (disableEndpoints) return disableEndpoints
|
if (disableEndpoints) return disableEndpoints
|
||||||
@@ -55,5 +55,7 @@ export async function checkFileAccess({
|
|||||||
if (!doc) {
|
if (!doc) {
|
||||||
throw new Forbidden(req.t)
|
throw new Forbidden(req.t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,16 +27,19 @@ export const getFile = async ({ collection, filename, req }: Args): Promise<Resp
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await checkFileAccess({
|
const accessResult = await checkFileAccess({
|
||||||
collection,
|
collection,
|
||||||
filename,
|
filename,
|
||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (accessResult instanceof Response) return accessResult
|
||||||
|
|
||||||
let response: Response = null
|
let response: Response = null
|
||||||
if (collection.config.upload.handlers?.length) {
|
if (collection.config.upload.handlers?.length) {
|
||||||
for (const handler of collection.config.upload.handlers) {
|
for (const handler of collection.config.upload.handlers) {
|
||||||
response = await handler(req, {
|
response = await handler(req, {
|
||||||
|
doc: accessResult,
|
||||||
params: {
|
params: {
|
||||||
collection: collection.config.slug,
|
collection: collection.config.slug,
|
||||||
filename,
|
filename,
|
||||||
|
|||||||
@@ -129,12 +129,15 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
|
|||||||
...url,
|
...url,
|
||||||
hooks: {
|
hooks: {
|
||||||
afterRead: [
|
afterRead: [
|
||||||
({ data }) =>
|
({ data, value }) => {
|
||||||
generateURL({
|
if (value) return value
|
||||||
|
|
||||||
|
return generateURL({
|
||||||
collectionSlug: collection.slug,
|
collectionSlug: collection.slug,
|
||||||
config,
|
config,
|
||||||
filename: data?.filename,
|
filename: data?.filename,
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -169,7 +172,9 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
|
|||||||
...url,
|
...url,
|
||||||
hooks: {
|
hooks: {
|
||||||
afterRead: [
|
afterRead: [
|
||||||
({ data }) => {
|
({ data, value }) => {
|
||||||
|
if (value) return value
|
||||||
|
|
||||||
const sizeFilename = data?.sizes?.[size.name]?.filename
|
const sizeFilename = data?.sizes?.[size.name]?.filename
|
||||||
|
|
||||||
if (sizeFilename) {
|
if (sizeFilename) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type express from 'express'
|
|||||||
import type serveStatic from 'serve-static'
|
import type serveStatic from 'serve-static'
|
||||||
import type { ResizeOptions, Sharp } from 'sharp'
|
import type { ResizeOptions, Sharp } from 'sharp'
|
||||||
|
|
||||||
|
import type { TypeWithID } from '../collections/config/types.js'
|
||||||
import type { PayloadRequestWithData } from '../types/index.js'
|
import type { PayloadRequestWithData } from '../types/index.js'
|
||||||
|
|
||||||
export type FileSize = {
|
export type FileSize = {
|
||||||
@@ -94,7 +95,7 @@ export type UploadConfig = {
|
|||||||
formatOptions?: ImageUploadFormatOptions
|
formatOptions?: ImageUploadFormatOptions
|
||||||
handlers?: ((
|
handlers?: ((
|
||||||
req: PayloadRequestWithData,
|
req: PayloadRequestWithData,
|
||||||
args: { params: { collection: string; filename: string } },
|
args: { doc: TypeWithID; params: { collection: string; filename: string } },
|
||||||
) => Promise<Response> | Response)[]
|
) => Promise<Response> | Response)[]
|
||||||
imageSizes?: ImageSize[]
|
imageSizes?: ImageSize[]
|
||||||
mimeTypes?: string[]
|
mimeTypes?: string[]
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const getFields = ({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = [...collection.fields]
|
const fields = [...collection.fields, ...(adapter.fields || [])]
|
||||||
|
|
||||||
// Inject a hook into all URL fields to generate URLs
|
// Inject a hook into all URL fields to generate URLs
|
||||||
|
|
||||||
@@ -107,6 +107,7 @@ export const getFields = ({
|
|||||||
name: size.name,
|
name: size.name,
|
||||||
type: 'group',
|
type: 'group',
|
||||||
fields: [
|
fields: [
|
||||||
|
...(adapter.fields || []),
|
||||||
{
|
{
|
||||||
...(existingSizeURLField || {}),
|
...(existingSizeURLField || {}),
|
||||||
...baseURLField,
|
...baseURLField,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const getAfterReadHook =
|
|||||||
if (disablePayloadAccessControl && filename) {
|
if (disablePayloadAccessControl && filename) {
|
||||||
url = await adapter.generateURL({
|
url = await adapter.generateURL({
|
||||||
collection,
|
collection,
|
||||||
|
data,
|
||||||
filename,
|
filename,
|
||||||
prefix,
|
prefix,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ export const getBeforeChangeHook =
|
|||||||
req.payload.logger.error(
|
req.payload.logger.error(
|
||||||
`There was an error while uploading files corresponding to the collection ${collection.slug} with filename ${data.filename}:`,
|
`There was an error while uploading files corresponding to the collection ${collection.slug} with filename ${data.filename}:`,
|
||||||
)
|
)
|
||||||
req.payload.logger.error(err)
|
req.payload.logger.error({ err })
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { FileData, ImageSize } from 'payload/types'
|
import type { Field, FileData, ImageSize } from 'payload/types'
|
||||||
import type { TypeWithID } from 'payload/types'
|
import type { TypeWithID } from 'payload/types'
|
||||||
import type { CollectionConfig, PayloadRequestWithData } from 'payload/types'
|
import type { CollectionConfig, PayloadRequestWithData } from 'payload/types'
|
||||||
|
|
||||||
@@ -30,17 +30,25 @@ export type HandleDelete = (args: {
|
|||||||
|
|
||||||
export type GenerateURL = (args: {
|
export type GenerateURL = (args: {
|
||||||
collection: CollectionConfig
|
collection: CollectionConfig
|
||||||
|
data: any
|
||||||
filename: string
|
filename: string
|
||||||
prefix?: string
|
prefix?: string
|
||||||
}) => Promise<string> | string
|
}) => Promise<string> | string
|
||||||
|
|
||||||
export type StaticHandler = (
|
export type StaticHandler = (
|
||||||
req: PayloadRequestWithData,
|
req: PayloadRequestWithData,
|
||||||
args: { params: { collection: string; filename: string } },
|
args: { doc?: TypeWithID; params: { collection: string; filename: string } },
|
||||||
) => Promise<Response> | Response
|
) => Promise<Response> | Response
|
||||||
|
|
||||||
export interface GeneratedAdapter {
|
export interface GeneratedAdapter {
|
||||||
generateURL: GenerateURL
|
/**
|
||||||
|
* Additional fields to be injected into the base collection and image sizes
|
||||||
|
*/
|
||||||
|
fields?: Field[]
|
||||||
|
/**
|
||||||
|
* Generates the public URL for a file
|
||||||
|
*/
|
||||||
|
generateURL?: GenerateURL
|
||||||
handleDelete: HandleDelete
|
handleDelete: HandleDelete
|
||||||
handleUpload: HandleUpload
|
handleUpload: HandleUpload
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
10
packages/storage-uploadthing/.eslintignore
Normal file
10
packages/storage-uploadthing/.eslintignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
7
packages/storage-uploadthing/.eslintrc.cjs
Normal file
7
packages/storage-uploadthing/.eslintrc.cjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
10
packages/storage-uploadthing/.prettierignore
Normal file
10
packages/storage-uploadthing/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
15
packages/storage-uploadthing/.swcrc
Normal file
15
packages/storage-uploadthing/.swcrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/swcrc",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"jsc": {
|
||||||
|
"target": "esnext",
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript",
|
||||||
|
"tsx": true,
|
||||||
|
"dts": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"type": "es6"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/storage-uploadthing/LICENSE.md
Normal file
22
packages/storage-uploadthing/LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018-2022 Payload CMS, LLC <info@payloadcms.com>
|
||||||
|
Portions Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
32
packages/storage-uploadthing/README.md
Normal file
32
packages/storage-uploadthing/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Uploadthing Storage for Payload (beta)
|
||||||
|
|
||||||
|
This package provides a way to use [uploadthing](https://uploadthing.com) with Payload.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm add @paylaodcms/storage-uploadthing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type.
|
||||||
|
- Get an API key from Uploadthing and set it as `apiKey` in the `options` object.
|
||||||
|
- `acl` is optional and defaults to `public-read`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default buildConfig({
|
||||||
|
collections: [Media],
|
||||||
|
plugins: [
|
||||||
|
uploadthingStorage({
|
||||||
|
collections: {
|
||||||
|
[mediaSlug]: true,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
apiKey: process.env.UPLOADTHING_SECRET,
|
||||||
|
acl: 'public-read',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
```
|
||||||
58
packages/storage-uploadthing/package.json
Normal file
58
packages/storage-uploadthing/package.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "@payloadcms/storage-uploadthing",
|
||||||
|
"version": "3.0.0-beta.25",
|
||||||
|
"description": "Payload storage adapter for uploadthing",
|
||||||
|
"homepage": "https://payloadcms.com",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/payloadcms/payload.git",
|
||||||
|
"directory": "packages/storage-uploadthing"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./src/index.ts",
|
||||||
|
"require": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm build:swc && pnpm build:types",
|
||||||
|
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
|
||||||
|
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||||
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||||
|
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||||
|
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@payloadcms/plugin-cloud-storage": "workspace:*",
|
||||||
|
"uploadthing": "^6.10.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"payload": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"payload": "workspace:*"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.20.2"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/storage-uploadthing/src/generateURL.ts
Normal file
10
packages/storage-uploadthing/src/generateURL.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { GenerateURL } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { getKeyFromFilename } from './utilities.js'
|
||||||
|
|
||||||
|
export const generateURL: GenerateURL = ({ data, filename, prefix = '' }) => {
|
||||||
|
const key = getKeyFromFilename(data, filename)
|
||||||
|
return `https://utfs.io/f/${path.posix.join(prefix, key || '')}`
|
||||||
|
}
|
||||||
35
packages/storage-uploadthing/src/handleDelete.ts
Normal file
35
packages/storage-uploadthing/src/handleDelete.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { HandleDelete } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { UTApi } from 'uploadthing/server'
|
||||||
|
|
||||||
|
import { APIError } from 'payload/errors'
|
||||||
|
|
||||||
|
import { getKeyFromFilename } from './utilities.js'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
utApi: UTApi
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHandleDelete = ({ utApi }: Args): HandleDelete => {
|
||||||
|
return async ({ doc, filename, req }) => {
|
||||||
|
const key = getKeyFromFilename(doc, filename)
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
req.payload.logger.error({
|
||||||
|
msg: `Error deleting file: ${filename} - unable to extract key from doc`,
|
||||||
|
})
|
||||||
|
throw new APIError(`Error deleting file: ${filename}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (key) {
|
||||||
|
await utApi.deleteFiles(key)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
req.payload.logger.error({
|
||||||
|
err,
|
||||||
|
msg: `Error deleting file with key: ${filename} - key: ${key}`,
|
||||||
|
})
|
||||||
|
throw new APIError(`Error deleting file: ${filename}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
packages/storage-uploadthing/src/handleUpload.ts
Normal file
57
packages/storage-uploadthing/src/handleUpload.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { HandleUpload } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { UTApi } from 'uploadthing/server'
|
||||||
|
|
||||||
|
import { APIError } from 'payload/errors'
|
||||||
|
import { UTFile } from 'uploadthing/server'
|
||||||
|
|
||||||
|
import type { ACL } from './index.js'
|
||||||
|
|
||||||
|
type HandleUploadArgs = {
|
||||||
|
acl: ACL
|
||||||
|
utApi: UTApi
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHandleUpload = ({ acl, utApi }: HandleUploadArgs): HandleUpload => {
|
||||||
|
return async ({ data, file }) => {
|
||||||
|
try {
|
||||||
|
const { buffer, filename, mimeType } = file
|
||||||
|
|
||||||
|
const blob = new Blob([buffer], { type: mimeType })
|
||||||
|
const res = await utApi.uploadFiles(new UTFile([blob], filename), { acl })
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
throw new APIError(`Error uploading file: ${res.error.code} - ${res.error.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching data.sizes entry
|
||||||
|
const foundSize = Object.keys(data.sizes || {}).find(
|
||||||
|
(key) => data.sizes?.[key]?.filename === filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (foundSize) {
|
||||||
|
data.sizes[foundSize]._key = res.data?.key
|
||||||
|
} else {
|
||||||
|
data._key = res.data?.key
|
||||||
|
data.filename = res.data?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
// Interrogate uploadthing error which returns FiberFailure
|
||||||
|
if ('toJSON' in error && typeof error.toJSON === 'function') {
|
||||||
|
const json = error.toJSON() as {
|
||||||
|
cause?: { defect?: { _id?: string; data?: { error?: string }; error?: string } }
|
||||||
|
}
|
||||||
|
if (json.cause?.defect?.error && json.cause.defect.data?.error) {
|
||||||
|
throw new APIError(
|
||||||
|
`Error uploading file with uploadthing: ${json.cause.defect.error} - ${json.cause.defect.data.error}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new APIError(`Error uploading file with uploadthing: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
packages/storage-uploadthing/src/index.ts
Normal file
132
packages/storage-uploadthing/src/index.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import type {
|
||||||
|
Adapter,
|
||||||
|
PluginOptions as CloudStoragePluginOptions,
|
||||||
|
CollectionOptions,
|
||||||
|
GeneratedAdapter,
|
||||||
|
} from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { Config, Plugin } from 'payload/config'
|
||||||
|
import type { Field } from 'payload/types'
|
||||||
|
import type { UTApiOptions } from 'uploadthing/types'
|
||||||
|
|
||||||
|
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'
|
||||||
|
import { UTApi } from 'uploadthing/server'
|
||||||
|
|
||||||
|
import { generateURL } from './generateURL.js'
|
||||||
|
import { getHandleDelete } from './handleDelete.js'
|
||||||
|
import { getHandleUpload } from './handleUpload.js'
|
||||||
|
import { getHandler } from './staticHandler.js'
|
||||||
|
|
||||||
|
export type UploadthingStorageOptions = {
|
||||||
|
/**
|
||||||
|
* Collection options to apply the adapter to.
|
||||||
|
*/
|
||||||
|
collections: Record<string, Omit<CollectionOptions, 'adapter'> | true>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to enable the plugin
|
||||||
|
*
|
||||||
|
* Default: true
|
||||||
|
*/
|
||||||
|
enabled?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploadthing Options
|
||||||
|
*/
|
||||||
|
options: UTApiOptions & {
|
||||||
|
/**
|
||||||
|
* @default 'public-read'
|
||||||
|
*/
|
||||||
|
acl?: ACL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadthingPlugin = (uploadthingStorageOptions: UploadthingStorageOptions) => Plugin
|
||||||
|
|
||||||
|
/** NOTE: not synced with uploadthing's internal types. Need to modify if more options added */
|
||||||
|
export type ACL = 'private' | 'public-read'
|
||||||
|
|
||||||
|
export const uploadthingStorage: UploadthingPlugin =
|
||||||
|
(uploadthingStorageOptions: UploadthingStorageOptions) =>
|
||||||
|
(incomingConfig: Config): Config => {
|
||||||
|
if (uploadthingStorageOptions.enabled === false) {
|
||||||
|
return incomingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default ACL to public-read
|
||||||
|
if (!uploadthingStorageOptions.options.acl) {
|
||||||
|
uploadthingStorageOptions.options.acl = 'public-read'
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter = uploadthingInternal(uploadthingStorageOptions, incomingConfig)
|
||||||
|
|
||||||
|
// Add adapter to each collection option object
|
||||||
|
const collectionsWithAdapter: CloudStoragePluginOptions['collections'] = Object.entries(
|
||||||
|
uploadthingStorageOptions.collections,
|
||||||
|
).reduce(
|
||||||
|
(acc, [slug, collOptions]) => ({
|
||||||
|
...acc,
|
||||||
|
[slug]: {
|
||||||
|
...(collOptions === true ? {} : collOptions),
|
||||||
|
|
||||||
|
// Disable payload access control if the ACL is public-read or not set
|
||||||
|
// ...(uploadthingStorageOptions.options.acl === 'public-read'
|
||||||
|
// ? { disablePayloadAccessControl: true }
|
||||||
|
// : {}),
|
||||||
|
|
||||||
|
adapter,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{} as Record<string, CollectionOptions>,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set disableLocalStorage: true for collections specified in the plugin options
|
||||||
|
const config = {
|
||||||
|
...incomingConfig,
|
||||||
|
collections: (incomingConfig.collections || []).map((collection) => {
|
||||||
|
if (!collectionsWithAdapter[collection.slug]) {
|
||||||
|
return collection
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...collection,
|
||||||
|
upload: {
|
||||||
|
...(typeof collection.upload === 'object' ? collection.upload : {}),
|
||||||
|
disableLocalStorage: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloudStoragePlugin({
|
||||||
|
collections: collectionsWithAdapter,
|
||||||
|
})(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadthingInternal(options: UploadthingStorageOptions, incomingConfig: Config): Adapter {
|
||||||
|
const fields: Field[] = [
|
||||||
|
{
|
||||||
|
name: '_key',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (): GeneratedAdapter => {
|
||||||
|
const {
|
||||||
|
options: { acl = 'public-read', ...utOptions },
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const utApi = new UTApi(utOptions)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'uploadthing',
|
||||||
|
fields,
|
||||||
|
generateURL,
|
||||||
|
handleDelete: getHandleDelete({ utApi }),
|
||||||
|
handleUpload: getHandleUpload({ acl, utApi }),
|
||||||
|
staticHandler: getHandler({ utApi }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
packages/storage-uploadthing/src/staticHandler.ts
Normal file
80
packages/storage-uploadthing/src/staticHandler.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import type { StaticHandler } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { Where } from 'payload/types'
|
||||||
|
import type { UTApi } from 'uploadthing/server'
|
||||||
|
|
||||||
|
import { getKeyFromFilename } from './utilities.js'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
utApi: UTApi
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHandler = ({ utApi }: Args): StaticHandler => {
|
||||||
|
return async (req, { doc, params: { collection, filename } }) => {
|
||||||
|
try {
|
||||||
|
const collectionConfig = req.payload.collections[collection]?.config
|
||||||
|
let retrievedDoc = doc
|
||||||
|
|
||||||
|
if (!retrievedDoc) {
|
||||||
|
const or: Where[] = [
|
||||||
|
{
|
||||||
|
filename: {
|
||||||
|
equals: filename,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (collectionConfig.upload.imageSizes) {
|
||||||
|
collectionConfig.upload.imageSizes.forEach(({ name }) => {
|
||||||
|
or.push({
|
||||||
|
[`sizes.${name}.filename`]: {
|
||||||
|
equals: filename,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await req.payload.db.findOne({
|
||||||
|
collection,
|
||||||
|
req,
|
||||||
|
where: { or },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result) retrievedDoc = result
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!retrievedDoc) {
|
||||||
|
return new Response(null, { status: 404, statusText: 'Not Found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = getKeyFromFilename(retrievedDoc, filename)
|
||||||
|
if (!key) {
|
||||||
|
return new Response(null, { status: 404, statusText: 'Not Found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url: signedURL } = await utApi.getSignedURL(key)
|
||||||
|
|
||||||
|
if (!signedURL) {
|
||||||
|
return new Response(null, { status: 404, statusText: 'Not Found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(signedURL)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return new Response(null, { status: 404, statusText: 'Not Found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob()
|
||||||
|
|
||||||
|
return new Response(blob, {
|
||||||
|
headers: new Headers({
|
||||||
|
'Content-Length': String(blob.size),
|
||||||
|
'Content-Type': blob.type,
|
||||||
|
}),
|
||||||
|
status: 200,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
req.payload.logger.error({ err, msg: 'Unexpected error in staticHandler' })
|
||||||
|
return new Response('Internal Server Error', { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
packages/storage-uploadthing/src/utilities.ts
Normal file
24
packages/storage-uploadthing/src/utilities.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Extract '_key' value from the doc safely
|
||||||
|
*/
|
||||||
|
export const getKeyFromFilename = (doc: unknown, filename: string) => {
|
||||||
|
if (
|
||||||
|
doc &&
|
||||||
|
typeof doc === 'object' &&
|
||||||
|
'filename' in doc &&
|
||||||
|
doc.filename === filename &&
|
||||||
|
'_key' in doc
|
||||||
|
) {
|
||||||
|
return doc._key as string
|
||||||
|
}
|
||||||
|
if (doc && typeof doc === 'object' && 'sizes' in doc) {
|
||||||
|
const sizes = doc.sizes
|
||||||
|
if (typeof sizes === 'object' && sizes !== null) {
|
||||||
|
for (const size of Object.values(sizes)) {
|
||||||
|
if (size?.filename === filename && '_key' in size) {
|
||||||
|
return size._key as string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/storage-uploadthing/tsconfig.json
Normal file
20
packages/storage-uploadthing/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true, // Make sure typescript knows that this module depends on their references
|
||||||
|
"noEmit": false /* Do not emit outputs. */,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||||
|
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||||
|
"strict": true,
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../payload" },
|
||||||
|
{ "path": "../plugin-cloud-storage" },
|
||||||
|
]
|
||||||
|
}
|
||||||
86
pnpm-lock.yaml
generated
86
pnpm-lock.yaml
generated
@@ -1421,6 +1421,19 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../payload
|
version: link:../payload
|
||||||
|
|
||||||
|
packages/storage-uploadthing:
|
||||||
|
dependencies:
|
||||||
|
'@payloadcms/plugin-cloud-storage':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../plugin-cloud-storage
|
||||||
|
uploadthing:
|
||||||
|
specifier: ^6.10.1
|
||||||
|
version: 6.10.1(next@14.3.0-canary.7)
|
||||||
|
devDependencies:
|
||||||
|
payload:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../payload
|
||||||
|
|
||||||
packages/storage-vercel-blob:
|
packages/storage-vercel-blob:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@payloadcms/plugin-cloud-storage':
|
'@payloadcms/plugin-cloud-storage':
|
||||||
@@ -1662,6 +1675,9 @@ importers:
|
|||||||
'@payloadcms/storage-s3':
|
'@payloadcms/storage-s3':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/storage-s3
|
version: link:../packages/storage-s3
|
||||||
|
'@payloadcms/storage-uploadthing':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/storage-uploadthing
|
||||||
'@payloadcms/storage-vercel-blob':
|
'@payloadcms/storage-vercel-blob':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/storage-vercel-blob
|
version: link:../packages/storage-vercel-blob
|
||||||
@@ -1704,6 +1720,9 @@ importers:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: 5.4.5
|
specifier: 5.4.5
|
||||||
version: 5.4.5
|
version: 5.4.5
|
||||||
|
uploadthing:
|
||||||
|
specifier: ^6.10.1
|
||||||
|
version: 6.10.1(next@14.3.0-canary.7)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -3323,6 +3342,15 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
superjson: 2.2.1
|
superjson: 2.2.1
|
||||||
|
|
||||||
|
/@effect/schema@0.66.14(effect@3.1.2)(fast-check@3.18.0):
|
||||||
|
resolution: {integrity: sha512-2Yc6gnXpcMmwQnbU2JUwDl0ckeOJmFZzteXn2jjVWuNi9PGv+jp2yK7jxv0pALcieuYwdR5tKkCRI7STuhEwfg==}
|
||||||
|
peerDependencies:
|
||||||
|
effect: ^3.1.2
|
||||||
|
fast-check: ^3.13.2
|
||||||
|
dependencies:
|
||||||
|
effect: 3.1.2
|
||||||
|
fast-check: 3.18.0
|
||||||
|
|
||||||
/@emotion/babel-plugin@11.11.0:
|
/@emotion/babel-plugin@11.11.0:
|
||||||
resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
|
resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6470,6 +6498,23 @@ packages:
|
|||||||
/@ungap/structured-clone@1.2.0:
|
/@ungap/structured-clone@1.2.0:
|
||||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||||
|
|
||||||
|
/@uploadthing/mime-types@0.2.9:
|
||||||
|
resolution: {integrity: sha512-7Ap2evP+niXNSXoOck4DUZUAKYrMRRpJ85n+ZxCuRsnr3iziOgv/Rt6es5SnMVgE/aOObxddOxbrxyOhISQWHQ==}
|
||||||
|
|
||||||
|
/@uploadthing/shared@6.7.1(@uploadthing/mime-types@0.2.9):
|
||||||
|
resolution: {integrity: sha512-4Imk46n+rwFaobfDuDSclYcH/OcpJA048Ww0il6nqT7EoXQ7Pa9NJfGkoNkS8+K8rT71IVljdaiaLEapgRxj0Q==}
|
||||||
|
peerDependencies:
|
||||||
|
'@uploadthing/mime-types': 0.2.9
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@uploadthing/mime-types':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@effect/schema': 0.66.14(effect@3.1.2)(fast-check@3.18.0)
|
||||||
|
'@uploadthing/mime-types': 0.2.9
|
||||||
|
effect: 3.1.2
|
||||||
|
fast-check: 3.18.0
|
||||||
|
std-env: 3.7.0
|
||||||
|
|
||||||
/@vercel/blob@0.22.3:
|
/@vercel/blob@0.22.3:
|
||||||
resolution: {integrity: sha512-l0t2KhbOO/I8ZNOl9zypYf1NE0837aO4/CPQNGR/RAxtj8FpdYKjhyUADUXj2gERLQmnhun+teaVs/G7vZJ/TQ==}
|
resolution: {integrity: sha512-l0t2KhbOO/I8ZNOl9zypYf1NE0837aO4/CPQNGR/RAxtj8FpdYKjhyUADUXj2gERLQmnhun+teaVs/G7vZJ/TQ==}
|
||||||
engines: {node: '>=16.14'}
|
engines: {node: '>=16.14'}
|
||||||
@@ -7847,7 +7892,6 @@ packages:
|
|||||||
/consola@3.2.3:
|
/consola@3.2.3:
|
||||||
resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
|
resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
|
||||||
engines: {node: ^14.18.0 || >=16.10.0}
|
engines: {node: ^14.18.0 || >=16.10.0}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/console-table-printer@2.11.2:
|
/console-table-printer@2.11.2:
|
||||||
resolution: {integrity: sha512-uuUHie0sfPP542TKGzPFal0W1wo1beuKAqIZdaavcONx8OoqdnJRKjkinbRTOta4FaCa1RcIL+7mMJWX3pQGVg==}
|
resolution: {integrity: sha512-uuUHie0sfPP542TKGzPFal0W1wo1beuKAqIZdaavcONx8OoqdnJRKjkinbRTOta4FaCa1RcIL+7mMJWX3pQGVg==}
|
||||||
@@ -8782,6 +8826,9 @@ packages:
|
|||||||
/ee-first@1.1.1:
|
/ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
|
/effect@3.1.2:
|
||||||
|
resolution: {integrity: sha512-XakSWck6w6ROqKyEys0tKE9K6Gx2p8W/09u2ZTEZZrneO5Z3QEdPhXzWTyC73kD5zUvfJinZLVIas8I1xoHaTg==}
|
||||||
|
|
||||||
/electron-to-chromium@1.4.730:
|
/electron-to-chromium@1.4.730:
|
||||||
resolution: {integrity: sha512-oJRPo82XEqtQAobHpJIR3zW5YO3sSRRkPz2an4yxi1UvqhsGm54vR/wzTFV74a3soDOJ8CKW7ajOOX5ESzddwg==}
|
resolution: {integrity: sha512-oJRPo82XEqtQAobHpJIR3zW5YO3sSRRkPz2an4yxi1UvqhsGm54vR/wzTFV74a3soDOJ8CKW7ajOOX5ESzddwg==}
|
||||||
|
|
||||||
@@ -9613,6 +9660,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==}
|
resolution: {integrity: sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/fast-check@3.18.0:
|
||||||
|
resolution: {integrity: sha512-/951xaT0kA40w0GXRsZXEwSTE7LugjZtSA/8vPgFkiPQ8wNp8tRvqWuNDHBgLxJYXtsK11e/7Q4ObkKW5BdTFQ==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
dependencies:
|
||||||
|
pure-rand: 6.1.0
|
||||||
|
|
||||||
/fast-copy@3.0.2:
|
/fast-copy@3.0.2:
|
||||||
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
|
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
|
||||||
|
|
||||||
@@ -15050,7 +15103,6 @@ packages:
|
|||||||
|
|
||||||
/std-env@3.7.0:
|
/std-env@3.7.0:
|
||||||
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
|
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/stop-iteration-iterator@1.0.0:
|
/stop-iteration-iterator@1.0.0:
|
||||||
resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==}
|
resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==}
|
||||||
@@ -15982,6 +16034,36 @@ packages:
|
|||||||
escalade: 3.1.2
|
escalade: 3.1.2
|
||||||
picocolors: 1.0.0
|
picocolors: 1.0.0
|
||||||
|
|
||||||
|
/uploadthing@6.10.1(next@14.3.0-canary.7):
|
||||||
|
resolution: {integrity: sha512-xqgEauaDYLlQ6NEsQWF+fpsUOuniWazd8ytSK2bCxcKFGakCkXO32GkFolW4iZZgkl7ATnHwN+JXG95FPdo74w==}
|
||||||
|
engines: {node: '>=18.13.0'}
|
||||||
|
peerDependencies:
|
||||||
|
express: '*'
|
||||||
|
fastify: '*'
|
||||||
|
h3: '*'
|
||||||
|
next: '*'
|
||||||
|
tailwindcss: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
express:
|
||||||
|
optional: true
|
||||||
|
fastify:
|
||||||
|
optional: true
|
||||||
|
h3:
|
||||||
|
optional: true
|
||||||
|
next:
|
||||||
|
optional: true
|
||||||
|
tailwindcss:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@effect/schema': 0.66.14(effect@3.1.2)(fast-check@3.18.0)
|
||||||
|
'@uploadthing/mime-types': 0.2.9
|
||||||
|
'@uploadthing/shared': 6.7.1(@uploadthing/mime-types@0.2.9)
|
||||||
|
consola: 3.2.3
|
||||||
|
effect: 3.1.2
|
||||||
|
fast-check: 3.18.0
|
||||||
|
next: 14.3.0-canary.7(@babel/core@7.24.4)(@playwright/test@1.43.0)(react-dom@18.2.0)(react@18.2.0)(sass@1.74.1)
|
||||||
|
std-env: 3.7.0
|
||||||
|
|
||||||
/uri-js@4.4.1:
|
/uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"@payloadcms/storage-azure": "workspace:*",
|
"@payloadcms/storage-azure": "workspace:*",
|
||||||
"@payloadcms/storage-gcs": "workspace:*",
|
"@payloadcms/storage-gcs": "workspace:*",
|
||||||
"@payloadcms/storage-s3": "workspace:*",
|
"@payloadcms/storage-s3": "workspace:*",
|
||||||
|
"@payloadcms/storage-uploadthing": "workspace:*",
|
||||||
"@payloadcms/storage-vercel-blob": "workspace:*",
|
"@payloadcms/storage-vercel-blob": "workspace:*",
|
||||||
"@payloadcms/translations": "workspace:*",
|
"@payloadcms/translations": "workspace:*",
|
||||||
"@payloadcms/ui": "workspace:*",
|
"@payloadcms/ui": "workspace:*",
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
"payload": "workspace:*",
|
"payload": "workspace:*",
|
||||||
"tempy": "^1.0.1",
|
"tempy": "^1.0.1",
|
||||||
"ts-essentials": "7.0.3",
|
"ts-essentials": "7.0.3",
|
||||||
"typescript": "5.4.5"
|
"typescript": "5.4.5",
|
||||||
|
"uploadthing": "^6.10.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
test/storage-uploadthing/.eslintrc.cjs
Normal file
8
test/storage-uploadthing/.eslintrc.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
ignorePatterns: ['payload-types.ts'],
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.eslint.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
34
test/storage-uploadthing/collections/Media.ts
Normal file
34
test/storage-uploadthing/collections/Media.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const Media: CollectionConfig = {
|
||||||
|
slug: 'media',
|
||||||
|
upload: {
|
||||||
|
disableLocalStorage: true,
|
||||||
|
resizeOptions: {
|
||||||
|
position: 'center',
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
},
|
||||||
|
imageSizes: [
|
||||||
|
{
|
||||||
|
height: 400,
|
||||||
|
width: 400,
|
||||||
|
crop: 'center',
|
||||||
|
name: 'square',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 900,
|
||||||
|
height: 450,
|
||||||
|
crop: 'center',
|
||||||
|
name: 'sixteenByNineMedium',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'alt',
|
||||||
|
label: 'Alt Text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
9
test/storage-uploadthing/collections/MediaWithPrefix.ts
Normal file
9
test/storage-uploadthing/collections/MediaWithPrefix.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const MediaWithPrefix: CollectionConfig = {
|
||||||
|
slug: 'media-with-prefix',
|
||||||
|
upload: {
|
||||||
|
disableLocalStorage: false,
|
||||||
|
},
|
||||||
|
fields: [],
|
||||||
|
}
|
||||||
16
test/storage-uploadthing/collections/Users.ts
Normal file
16
test/storage-uploadthing/collections/Users.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const Users: CollectionConfig = {
|
||||||
|
slug: 'users',
|
||||||
|
auth: true,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
// Email added by default
|
||||||
|
// Add more fields as needed
|
||||||
|
],
|
||||||
|
}
|
||||||
43
test/storage-uploadthing/config.ts
Normal file
43
test/storage-uploadthing/config.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { uploadthingStorage } from '@payloadcms/storage-uploadthing'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
|
import { devUser } from '../credentials.js'
|
||||||
|
import { Media } from './collections/Media.js'
|
||||||
|
import { MediaWithPrefix } from './collections/MediaWithPrefix.js'
|
||||||
|
import { Users } from './collections/Users.js'
|
||||||
|
import { mediaSlug } from './shared.js'
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
// Load test env creds
|
||||||
|
dotenv.config({
|
||||||
|
path: path.resolve(dirname, './.env'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default buildConfigWithDefaults({
|
||||||
|
collections: [Media, MediaWithPrefix, Users],
|
||||||
|
onInit: async (payload) => {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
uploadthingStorage({
|
||||||
|
collections: {
|
||||||
|
[mediaSlug]: true,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
apiKey: process.env.UPLOADTHING_SECRET,
|
||||||
|
acl: 'public-read',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
3
test/storage-uploadthing/shared.ts
Normal file
3
test/storage-uploadthing/shared.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const mediaSlug = 'media'
|
||||||
|
export const mediaWithPrefixSlug = 'media-with-prefix'
|
||||||
|
export const prefix = 'test-prefix'
|
||||||
13
test/storage-uploadthing/tsconfig.eslint.json
Normal file
13
test/storage-uploadthing/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// extend your base config to share compilerOptions, etc
|
||||||
|
//"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
// ensure that nobody can accidentally use this config for a build
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
// whatever paths you intend to lint
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@payload-config": [
|
"@payload-config": [
|
||||||
"./test/live-preview/config.ts"
|
"./test/_community/config.ts"
|
||||||
],
|
],
|
||||||
"@payloadcms/live-preview": [
|
"@payloadcms/live-preview": [
|
||||||
"./packages/live-preview/src"
|
"./packages/live-preview/src"
|
||||||
|
|||||||
Reference in New Issue
Block a user