feat: storage-uploadthing package (#6316)

Co-authored-by: James <james@trbl.design>
This commit is contained in:
Elliot DeNolf
2024-05-10 17:05:35 -04:00
committed by GitHub
parent ea84e82ad5
commit ed880d5018
34 changed files with 774 additions and 17 deletions

8
.vscode/launch.json vendored
View File

@@ -16,6 +16,14 @@
"request": "launch",
"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",
"cwd": "${workspaceFolder}",

View File

@@ -33,6 +33,11 @@
"build:plugins": "turbo build --filter \"@payloadcms/plugin-*\"",
"build:richtext-lexical": "turbo build --filter richtext-lexical",
"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:translations": "turbo build --filter translations",
"build:ui": "turbo build --filter ui",

View File

@@ -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 { Forbidden } from 'payload/errors'
@@ -13,7 +13,7 @@ export async function checkFileAccess({
collection: Collection
filename: string
req: PayloadRequestWithData
}) {
}): Promise<Response | TypeWithID> {
const { config } = collection
const disableEndpoints = endpointsAreDisabled({ endpoints: config.endpoints, request: req })
if (disableEndpoints) return disableEndpoints
@@ -55,5 +55,7 @@ export async function checkFileAccess({
if (!doc) {
throw new Forbidden(req.t)
}
return doc
}
}

View File

@@ -27,16 +27,19 @@ export const getFile = async ({ collection, filename, req }: Args): Promise<Resp
)
}
await checkFileAccess({
const accessResult = await checkFileAccess({
collection,
filename,
req,
})
if (accessResult instanceof Response) return accessResult
let response: Response = null
if (collection.config.upload.handlers?.length) {
for (const handler of collection.config.upload.handlers) {
response = await handler(req, {
doc: accessResult,
params: {
collection: collection.config.slug,
filename,

View File

@@ -129,12 +129,15 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
...url,
hooks: {
afterRead: [
({ data }) =>
generateURL({
({ data, value }) => {
if (value) return value
return generateURL({
collectionSlug: collection.slug,
config,
filename: data?.filename,
}),
})
},
],
},
},
@@ -169,7 +172,9 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
...url,
hooks: {
afterRead: [
({ data }) => {
({ data, value }) => {
if (value) return value
const sizeFilename = data?.sizes?.[size.name]?.filename
if (sizeFilename) {

View File

@@ -2,6 +2,7 @@ import type express from 'express'
import type serveStatic from 'serve-static'
import type { ResizeOptions, Sharp } from 'sharp'
import type { TypeWithID } from '../collections/config/types.js'
import type { PayloadRequestWithData } from '../types/index.js'
export type FileSize = {
@@ -94,7 +95,7 @@ export type UploadConfig = {
formatOptions?: ImageUploadFormatOptions
handlers?: ((
req: PayloadRequestWithData,
args: { params: { collection: string; filename: string } },
args: { doc: TypeWithID; params: { collection: string; filename: string } },
) => Promise<Response> | Response)[]
imageSizes?: ImageSize[]
mimeTypes?: string[]

View File

@@ -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
@@ -107,6 +107,7 @@ export const getFields = ({
name: size.name,
type: 'group',
fields: [
...(adapter.fields || []),
{
...(existingSizeURLField || {}),
...baseURLField,

View File

@@ -20,6 +20,7 @@ export const getAfterReadHook =
if (disablePayloadAccessControl && filename) {
url = await adapter.generateURL({
collection,
data,
filename,
prefix,
})

View File

@@ -54,7 +54,8 @@ export const getBeforeChangeHook =
req.payload.logger.error(
`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
}

View File

@@ -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 { CollectionConfig, PayloadRequestWithData } from 'payload/types'
@@ -30,17 +30,25 @@ export type HandleDelete = (args: {
export type GenerateURL = (args: {
collection: CollectionConfig
data: any
filename: string
prefix?: string
}) => Promise<string> | string
export type StaticHandler = (
req: PayloadRequestWithData,
args: { params: { collection: string; filename: string } },
args: { doc?: TypeWithID; params: { collection: string; filename: string } },
) => Promise<Response> | Response
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
handleUpload: HandleUpload
name: string

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View File

@@ -0,0 +1,7 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
}

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View 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"
}
}

View 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.

View 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',
},
}),
],
})
```

View 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"
}
}

View 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 || '')}`
}

View 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}`)
}
}
}

View 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}`)
}
}
}
}
}

View 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 }),
}
}
}

View 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 })
}
}
}

View 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
}
}
}
}
}

View 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
View File

@@ -1421,6 +1421,19 @@ importers:
specifier: workspace:*
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:
dependencies:
'@payloadcms/plugin-cloud-storage':
@@ -1662,6 +1675,9 @@ importers:
'@payloadcms/storage-s3':
specifier: workspace:*
version: link:../packages/storage-s3
'@payloadcms/storage-uploadthing':
specifier: workspace:*
version: link:../packages/storage-uploadthing
'@payloadcms/storage-vercel-blob':
specifier: workspace:*
version: link:../packages/storage-vercel-blob
@@ -1704,6 +1720,9 @@ importers:
typescript:
specifier: 5.4.5
version: 5.4.5
uploadthing:
specifier: ^6.10.1
version: 6.10.1(next@14.3.0-canary.7)
packages:
@@ -3323,6 +3342,15 @@ packages:
dependencies:
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:
resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
dependencies:
@@ -6470,6 +6498,23 @@ packages:
/@ungap/structured-clone@1.2.0:
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:
resolution: {integrity: sha512-l0t2KhbOO/I8ZNOl9zypYf1NE0837aO4/CPQNGR/RAxtj8FpdYKjhyUADUXj2gERLQmnhun+teaVs/G7vZJ/TQ==}
engines: {node: '>=16.14'}
@@ -7847,7 +7892,6 @@ packages:
/consola@3.2.3:
resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
engines: {node: ^14.18.0 || >=16.10.0}
dev: true
/console-table-printer@2.11.2:
resolution: {integrity: sha512-uuUHie0sfPP542TKGzPFal0W1wo1beuKAqIZdaavcONx8OoqdnJRKjkinbRTOta4FaCa1RcIL+7mMJWX3pQGVg==}
@@ -8782,6 +8826,9 @@ packages:
/ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
/effect@3.1.2:
resolution: {integrity: sha512-XakSWck6w6ROqKyEys0tKE9K6Gx2p8W/09u2ZTEZZrneO5Z3QEdPhXzWTyC73kD5zUvfJinZLVIas8I1xoHaTg==}
/electron-to-chromium@1.4.730:
resolution: {integrity: sha512-oJRPo82XEqtQAobHpJIR3zW5YO3sSRRkPz2an4yxi1UvqhsGm54vR/wzTFV74a3soDOJ8CKW7ajOOX5ESzddwg==}
@@ -9613,6 +9660,12 @@ packages:
resolution: {integrity: sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==}
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:
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
@@ -15050,7 +15103,6 @@ packages:
/std-env@3.7.0:
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
dev: true
/stop-iteration-iterator@1.0.0:
resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==}
@@ -15982,6 +16034,36 @@ packages:
escalade: 3.1.2
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:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies:

View File

@@ -36,6 +36,7 @@
"@payloadcms/storage-azure": "workspace:*",
"@payloadcms/storage-gcs": "workspace:*",
"@payloadcms/storage-s3": "workspace:*",
"@payloadcms/storage-uploadthing": "workspace:*",
"@payloadcms/storage-vercel-blob": "workspace:*",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
@@ -49,6 +50,7 @@
"payload": "workspace:*",
"tempy": "^1.0.1",
"ts-essentials": "7.0.3",
"typescript": "5.4.5"
"typescript": "5.4.5",
"uploadthing": "^6.10.1"
}
}

View File

@@ -0,0 +1,8 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
ignorePatterns: ['payload-types.ts'],
parserOptions: {
project: ['./tsconfig.eslint.json'],
tsconfigRootDir: __dirname,
},
}

View 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',
},
],
}

View File

@@ -0,0 +1,9 @@
import type { CollectionConfig } from 'payload/types'
export const MediaWithPrefix: CollectionConfig = {
slug: 'media-with-prefix',
upload: {
disableLocalStorage: false,
},
fields: [],
}

View 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
],
}

View 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',
},
}),
],
})

View File

@@ -0,0 +1,3 @@
export const mediaSlug = 'media'
export const mediaWithPrefixSlug = 'media-with-prefix'
export const prefix = 'test-prefix'

View 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"
]
}

View File

@@ -37,7 +37,7 @@
],
"paths": {
"@payload-config": [
"./test/live-preview/config.ts"
"./test/_community/config.ts"
],
"@payloadcms/live-preview": [
"./packages/live-preview/src"