Compare commits

...

9 Commits

122 changed files with 1066 additions and 486 deletions

View File

@@ -599,7 +599,7 @@ export const Orders: CollectionConfig = {
{
path: '/:id/tracking',
method: 'get',
handler: (req) => {
handler: async (req) => {
const tracking = await getTrackingInfo(req.params.id)
if (!tracking) {
@@ -614,7 +614,7 @@ export const Orders: CollectionConfig = {
{
path: '/:id/tracking',
method: 'post',
handler: (req) => {
handler: async (req) => {
// `data` is not automatically appended to the request
// if you would like to read the body of the request
// you can use `data = await req.json()`
@@ -654,7 +654,7 @@ import { addDataAndFileToRequest } from '@payloadcms/next/utilities'
{
path: '/:id/tracking',
method: 'post',
handler: (req) => {
handler: async (req) => {
await addDataAndFileToRequest(req)
await req.payload.update({
collection: 'tracking',
@@ -680,7 +680,7 @@ import { addLocalesToRequestFromData } from '@payloadcms/next/utilities'
{
path: '/:id/tracking',
method: 'post',
handler: (req) => {
handler: async (req) => {
await addLocalesToRequestFromData(req)
// you now can access req.locale & req.fallbackLocale
return Response.json({ message: 'success' })

View File

@@ -1,4 +1,4 @@
# Payload Multi-Tenant Example
# Payload Multi-Tenant Example (Single Domain)
This example demonstrates how to achieve a multi-tenancy in [Payload](https://github.com/payloadcms/payload) on a single domain. Tenants are separated by a `Tenants` collection.
@@ -6,12 +6,16 @@ This example demonstrates how to achieve a multi-tenancy in [Payload](https://gi
To spin up this example locally, follow these steps:
1. First clone the repo
2. `cd YOUR_PROJECT_REPO && cp .env.example .env`
3. `pnpm i && pnpm dev`
4. run `yarn seed` to seed the database
5. open `http://localhost:3000/admin` to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
1. Clone this repo
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
- Press `y` when prompted to seed the database
1. `open http://localhost:3000` to access the home page
1. `open http://localhost:3000/admin` to access the admin panel
- Login with email `demo@payloadcms.com` and password `demo`
## How it works

View File

@@ -0,0 +1,8 @@
import { withPayload } from '@payloadcms/next/withPayload'
/** @type {import('next').NextConfig} */
const nextConfig = {
// Your Next.js config here
}
export default withPayload(nextConfig)

View File

@@ -1,21 +1,18 @@
{
"name": "multi-tenant-single-domain",
"description": "An example of a multi tenant application, using a single domain",
"version": "1.0.0",
"description": "An example of a multi tenant application, using a single domain",
"license": "MIT",
"type": "module",
"engines": {
"node": "^18.20.2 || >=20.9.0"
},
"scripts": {
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --turbo",
"_dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
"generate:types": "payload generate:types",
"dev": "cross-env NODE_OPTIONS=--no-deprecation && pnpm seed && next dev --turbo",
"generate:schema": "payload-graphql generate:schema",
"seed": "tsx ./scripts/seed.ts"
"generate:types": "payload generate:types",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"seed": "npm run payload migrate:fresh",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
},
"dependencies": {
"@payloadcms/db-mongodb": "3.0.0-beta.58",
@@ -42,6 +39,9 @@
"tsx": "^4.16.2",
"typescript": "5.5.2"
},
"engines": {
"node": "^18.20.2 || >=20.9.0"
},
"pnpm": {
"overrides": {
"@types/react": "npm:types-react@19.0.0-beta.2",

View File

@@ -1,209 +0,0 @@
/**
* This is an example of a standalone script that loads in the Payload config
* and uses the Payload Local API to query the database.
*/
process.env.PAYLOAD_DROP_DATABASE = 'true'
import type { Payload, RequiredDataFromCollectionSlug } from 'payload'
import { getPayload } from 'payload'
import { importConfig } from 'payload/node'
async function findOrCreateTenant({ data, payload }: { data: any; payload: Payload }) {
const tenantsQuery = await payload.find({
collection: 'tenants',
where: {
slug: {
equals: data.slug,
},
},
})
if (tenantsQuery.docs?.[0]) return tenantsQuery.docs[0]
return payload.create({
collection: 'tenants',
data,
})
}
async function findOrCreateUser({
data,
payload,
}: {
data: RequiredDataFromCollectionSlug<'users'>
payload: Payload
}) {
const usersQuery = await payload.find({
collection: 'users',
where: {
email: {
equals: data.email,
},
},
})
if (usersQuery.docs?.[0]) return usersQuery.docs[0]
return payload.create({
collection: 'users',
data,
})
}
async function findOrCreatePage({ data, payload }: { data: any; payload: Payload }) {
const pagesQuery = await payload.find({
collection: 'pages',
where: {
slug: {
equals: data.slug,
},
},
})
if (pagesQuery.docs?.[0]) return pagesQuery.docs[0]
return payload.create({
collection: 'pages',
data,
})
}
async function run() {
const awaitedConfig = await importConfig('../src/payload.config.ts')
const payload = await getPayload({ config: awaitedConfig })
const tenant1 = await findOrCreateTenant({
data: {
name: 'Tenant 1',
slug: 'tenant-1',
},
payload,
})
const tenant2 = await findOrCreateTenant({
data: {
name: 'Tenant 2',
slug: 'tenant-2',
public: true,
},
payload,
})
const tenant3 = await findOrCreateTenant({
data: {
name: 'Tenant 3',
slug: 'tenant-3',
},
payload,
})
await findOrCreateUser({
data: {
email: 'demo@payloadcms.com',
password: 'demo',
roles: ['super-admin'],
},
payload,
})
await findOrCreateUser({
data: {
email: 'tenant1@payloadcms.com',
password: 'test',
tenants: [
{
roles: ['tenant-admin'],
tenant: tenant1.id,
},
],
username: 'tenant1',
},
payload,
})
await findOrCreateUser({
data: {
email: 'tenant2@payloadcms.com',
password: 'test',
tenants: [
{
roles: ['tenant-admin'],
tenant: tenant2.id,
},
],
username: 'tenant2',
},
payload,
})
await findOrCreateUser({
data: {
email: 'tenant3@payloadcms.com',
password: 'test',
tenants: [
{
roles: ['tenant-admin'],
tenant: tenant3.id,
},
],
username: 'tenant3',
},
payload,
})
await findOrCreateUser({
data: {
email: 'multi-admin@payloadcms.com',
password: 'test',
tenants: [
{
roles: ['tenant-admin'],
tenant: tenant1.id,
},
{
roles: ['tenant-admin'],
tenant: tenant2.id,
},
{
roles: ['tenant-admin'],
tenant: tenant3.id,
},
],
username: 'tenant3',
},
payload,
})
await findOrCreatePage({
data: {
slug: 'home',
tenant: tenant1.id,
title: 'Page for Tenant 1',
},
payload,
})
await findOrCreatePage({
data: {
slug: 'home',
tenant: tenant2.id,
title: 'Page for Tenant 2',
},
payload,
})
await findOrCreatePage({
data: {
slug: 'home',
tenant: tenant3.id,
title: 'Page for Tenant 3',
},
payload,
})
process.exit(0)
}
run().catch(console.error)

View File

@@ -2,7 +2,7 @@ import type { Where } from 'payload'
import configPromise from '@payload-config'
import { getPayloadHMR } from '@payloadcms/next/utilities'
import { headers as getHeaders } from 'next/headers.js'
import { headers as getHeaders } from 'next/headers'
import { notFound, redirect } from 'next/navigation'
import React from 'react'

View File

@@ -1 +0,0 @@
export default {};

View File

@@ -1,5 +0,0 @@
import type { PayloadRequest } from "payload";
export const isPayloadAdminPanel = (req: PayloadRequest) => {
return req.headers.has('referer') && req.headers.get('referer')?.startsWith(`${process.env.NEXT_PUBLIC_SERVER_URL}${req.payload.config.routes.admin}`)
}

View File

@@ -2,8 +2,8 @@ import type { Access } from 'payload'
import { parseCookies } from 'payload'
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const filterByTenantRead: Access = (args) => {
const req = args.req

View File

@@ -2,7 +2,7 @@ import type { FieldHook } from 'payload'
import { ValidationError } from 'payload'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const ensureUniqueSlug: FieldHook = async ({ data, originalDoc, req, value }) => {
// if value is unchanged, skip validation

View File

@@ -1,10 +1,10 @@
import type { CollectionConfig } from 'payload'
import { tenantField } from '../../fields/TenantField/index.js'
import { isPayloadAdminPanel } from '../../utilities/isPayloadAdminPanel.js'
import { canMutatePage, filterByTenantRead } from './access/byTenant.js'
import { externalReadAccess } from './access/externalReadAccess.js'
import { ensureUniqueSlug } from './hooks/ensureUniqueSlug.js'
import { tenantField } from '../../fields/TenantField'
import { isPayloadAdminPanel } from '../../utilities/isPayloadAdminPanel'
import { canMutatePage, filterByTenantRead } from './access/byTenant'
import { externalReadAccess } from './access/externalReadAccess'
import { ensureUniqueSlug } from './hooks/ensureUniqueSlug'
export const Pages: CollectionConfig = {
slug: 'pages',

View File

@@ -2,8 +2,8 @@ import type { Access } from 'payload'
import { parseCookies } from 'payload'
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const filterByTenantRead: Access = (args) => {
const req = args.req

View File

@@ -1,7 +1,7 @@
import type { Access } from 'payload'
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const tenantRead: Access = (args) => {
const req = args.req

View File

@@ -1,7 +1,7 @@
import type { CollectionConfig } from 'payload'
import { isSuperAdmin } from '../../access/isSuperAdmin.js'
import { canMutateTenant, filterByTenantRead } from './access/byTenant.js'
import { isSuperAdmin } from '../../access/isSuperAdmin'
import { canMutateTenant, filterByTenantRead } from './access/byTenant'
export const Tenants: CollectionConfig = {
slug: 'tenants',

View File

@@ -2,8 +2,8 @@ import type { Access } from 'payload'
import type { User } from '../../../../payload-types'
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const createAccess: Access<User> = (args) => {
const { req } = args

View File

@@ -1,6 +1,6 @@
import type { Access } from 'payload'
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
import { isAccessingSelf } from './isAccessingSelf.js'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { isAccessingSelf } from './isAccessingSelf'
export const isSuperAdminOrSelf: Access = (args) => isSuperAdmin(args) || isAccessingSelf(args)

View File

@@ -1,10 +1,11 @@
import type { User } from '@/payload-types'
import type { Access, Where } from 'payload'
import { isSuperAdmin } from '@/cms/access/isSuperAdmin'
import { getTenantAdminTenantAccessIDs } from '@/cms/utilities/getTenantAccessIDs'
import { parseCookies } from 'payload'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const readAccess: Access<User> = (args) => {
const { req } = args
if (!req?.user) return false

View File

@@ -1,7 +1,7 @@
import type { Access } from 'payload'
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const updateAndDeleteAccess: Access = (args) => {
const { req } = args

View File

@@ -2,7 +2,7 @@ import type { FieldHook } from 'payload'
import { ValidationError } from 'payload'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req, value }) => {
// if value is unchanged, skip validation

View File

@@ -2,12 +2,12 @@ import type { CollectionConfig } from 'payload'
import type { User } from '../../../payload-types'
import { getTenantAdminTenantAccessIDs } from '../../utilities/getTenantAccessIDs.js'
import { createAccess } from './access/create.js'
import { getTenantAdminTenantAccessIDs } from '../../utilities/getTenantAccessIDs'
import { createAccess } from './access/create'
import { readAccess } from './access/read'
import { updateAndDeleteAccess } from './access/updateAndDelete.js'
import { externalUsersLogin } from './endpoints/externalUsersLogin.js'
import { ensureUniqueUsername } from './hooks/ensureUniqueUsername.js'
import { updateAndDeleteAccess } from './access/updateAndDelete'
import { externalUsersLogin } from './endpoints/externalUsersLogin'
import { ensureUniqueUsername } from './hooks/ensureUniqueUsername'
const Users: CollectionConfig = {
slug: 'users',

View File

@@ -1,7 +1,7 @@
import { cookies as getCookies } from 'next/headers'
import React from 'react'
import { TenantSelector } from './index.client.js'
import { TenantSelector } from './index.client'
export const TenantSelectorRSC = () => {
const cookies = getCookies()

View File

@@ -1,7 +1,7 @@
import type { FieldAccess } from 'payload'
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const tenantFieldUpdate: FieldAccess = (args) => {
const tenantIDs = getTenantAccessIDs(args.req.user)

View File

@@ -1,6 +1,6 @@
import type { FieldHook } from 'payload'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const autofillTenant: FieldHook = ({ req, value }) => {
// If there is no value,

View File

@@ -1,9 +1,9 @@
import type { Field } from 'payload'
import { isSuperAdmin } from '../../access/isSuperAdmin.js'
import { tenantFieldUpdate } from './access/update.js'
import { TenantFieldComponent } from './components/Field.js'
import { autofillTenant } from './hooks/autofillTenant.js'
import { isSuperAdmin } from '../../access/isSuperAdmin'
import { tenantFieldUpdate } from './access/update'
import { TenantFieldComponent } from './components/Field'
import { autofillTenant } from './hooks/autofillTenant'
export const tenantField: Field = {
name: 'tenant',

View File

@@ -0,0 +1,131 @@
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
export async function up({ payload }: MigrateUpArgs): Promise<void> {
await payload.create({
collection: 'users',
data: {
email: 'demo@payloadcms.com',
password: 'demo',
roles: ['super-admin'],
},
})
const tenant1 = await payload.create({
collection: 'tenants',
data: {
name: 'Tenant 1',
slug: 'tenant-1',
},
})
const tenant2 = await payload.create({
collection: 'tenants',
data: {
name: 'Tenant 2',
slug: 'tenant-2',
},
})
const tenant3 = await payload.create({
collection: 'tenants',
data: {
name: 'Tenant 3',
slug: 'tenant-3',
},
})
await payload.create({
collection: 'users',
data: {
email: 'tenant1@payloadcms.com',
password: 'test',
tenants: [
{
roles: ['tenant-admin'],
tenant: tenant1.id,
},
],
username: 'tenant1',
},
})
await payload.create({
collection: 'users',
data: {
email: 'tenant2@payloadcms.com',
password: 'test',
tenants: [
{
roles: ['tenant-admin'],
tenant: tenant2.id,
},
],
username: 'tenant2',
},
})
await payload.create({
collection: 'users',
data: {
email: 'tenant3@payloadcms.com',
password: 'test',
tenants: [
{
roles: ['tenant-admin'],
tenant: tenant3.id,
},
],
username: 'tenant3',
},
})
await payload.create({
collection: 'users',
data: {
email: 'multi-admin@payloadcms.com',
password: 'test',
tenants: [
{
roles: ['tenant-admin'],
tenant: tenant1.id,
},
{
roles: ['tenant-admin'],
tenant: tenant2.id,
},
{
roles: ['tenant-admin'],
tenant: tenant3.id,
},
],
username: 'tenant3',
},
})
await payload.create({
collection: 'pages',
data: {
slug: 'home',
tenant: tenant1.id,
title: 'Page for Tenant 1',
},
})
await payload.create({
collection: 'pages',
data: {
slug: 'home',
tenant: tenant2.id,
title: 'Page for Tenant 2',
},
})
await payload.create({
collection: 'pages',
data: {
slug: 'home',
tenant: tenant3.id,
title: 'Page for Tenant 3',
},
})
}

View File

@@ -0,0 +1 @@
export default {}

View File

@@ -131,4 +131,4 @@ export interface Auth {
declare module 'payload' {
export interface GeneratedTypes extends Config {}
}
}

View File

@@ -4,10 +4,10 @@ import path from 'path'
import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'
import { Pages } from './cms/collections/Pages/index.js'
import { Tenants } from './cms/collections/Tenants/index.js'
import Users from './cms/collections/Users/index.js'
import { TenantSelectorRSC } from './cms/components/TenantSelector/index.js'
import { Pages } from './collections/Pages'
import { Tenants } from './collections/Tenants'
import Users from './collections/Users'
import { TenantSelectorRSC } from './components/TenantSelector'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

View File

@@ -0,0 +1,10 @@
import type { PayloadRequest } from 'payload'
export const isPayloadAdminPanel = (req: PayloadRequest) => {
return (
req.headers.has('referer') &&
req.headers
.get('referer')
?.startsWith(`${process.env.NEXT_PUBLIC_SERVER_URL}${req.payload.config.routes.admin}`)
)
}

View File

@@ -27,10 +27,10 @@
"./src/*"
],
"@payload-config": [
"./src/payload.config.ts"
"src/payload.config.ts"
],
"@payload-types": [
"./src/payload-types.ts"
"src/payload-types.ts"
]
},
"target": "ES2017"

View File

@@ -47,6 +47,16 @@
"test": "jest",
"typecheck": "tsc"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@clack/prompts": "^0.7.0",
"@sindresorhus/slugify": "^1.1.0",

View File

@@ -32,6 +32,16 @@
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"bson-objectid": "2.0.4",
"deepmerge": "4.3.1",

View File

@@ -43,6 +43,16 @@
"prepublishOnly": "pnpm clean && pnpm turbo build",
"renamePredefinedMigrations": "tsx ./scripts/renamePredefinedMigrations.ts"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@libsql/client": "^0.5.2",
"console-table-printer": "2.11.2",

View File

@@ -31,6 +31,16 @@
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"nodemailer": "6.9.10"
},

View File

@@ -31,6 +31,16 @@
"prepublishOnly": "pnpm clean && pnpm turbo build",
"test": "jest"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"devDependencies": {
"@types/jest": "29.5.12",
"jest": "^29.7.0",

View File

@@ -16,6 +16,16 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@eslint-react/eslint-plugin": "1.5.25-next.4",
"@eslint/js": "9.6.0",

View File

@@ -16,6 +16,16 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@eslint-react/eslint-plugin": "1.5.25-next.4",
"@eslint/js": "9.6.0",

View File

@@ -40,6 +40,16 @@
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"graphql-scalars": "1.22.2",
"pluralize": "8.0.0",

View File

@@ -31,6 +31,16 @@
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@payloadcms/live-preview": "workspace:*"
},

View File

@@ -31,6 +31,16 @@
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@payloadcms/live-preview": "workspace:*"
},

View File

@@ -31,6 +31,16 @@
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"payload": "workspace:*"

View File

@@ -61,6 +61,16 @@
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@dnd-kit/core": "6.0.8",
"@payloadcms/graphql": "workspace:*",

View File

@@ -1,7 +1,7 @@
import type { Collection, PayloadRequest, SanitizedConfig } from 'payload'
import httpStatus from 'http-status'
import { APIError } from 'payload'
import { APIError, APIErrorName, ValidationErrorName } from 'payload'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
import { headersWithCors } from '../../utilities/headersWithCors.js'
@@ -16,7 +16,7 @@ const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorRes
// Payload 'ValidationError' and 'APIError'
if (
(proto.constructor.name === 'ValidationError' || proto.constructor.name === 'APIError') &&
(proto.constructor.name === ValidationErrorName || proto.constructor.name === APIErrorName) &&
incoming.data
) {
return {
@@ -31,7 +31,7 @@ const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorRes
}
// Mongoose 'ValidationError': https://mongoosejs.com/docs/api/error.html#Error.ValidationError
if (proto.constructor.name === 'ValidationError' && 'errors' in incoming && incoming.errors) {
if (proto.constructor.name === ValidationErrorName && 'errors' in incoming && incoming.errors) {
return {
errors: Object.keys(incoming.errors).reduce((acc, key) => {
acc.push({

View File

@@ -83,6 +83,16 @@
"prepublishOnly": "pnpm clean && pnpm turbo build",
"pretest": "pnpm build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@next/env": "^15.0.0-rc.0",
"@payloadcms/translations": "workspace:*",

View File

@@ -0,0 +1,172 @@
import type { Config } from '../../config/types.js'
import type { CollectionConfig, Field } from '../../index.js'
import { ReservedFieldName } from '../../errors/index.js'
import { sanitizeCollection } from './sanitize.js'
describe('reservedFieldNames - collections -', () => {
const config = {
collections: [],
globals: [],
} as Partial<Config>
describe('uploads -', () => {
const collectionWithUploads: CollectionConfig = {
slug: 'collection-with-uploads',
fields: [],
upload: true,
}
it('should throw on file', async () => {
const fields: Field[] = [
{
name: 'file',
type: 'text',
label: 'some-collection',
},
]
await expect(async () => {
await sanitizeCollection(
// @ts-expect-error
{
...config,
collections: [
{
...collectionWithUploads,
fields,
},
],
},
{
...collectionWithUploads,
fields,
},
)
}).rejects.toThrow(ReservedFieldName)
})
it('should not throw on a custom field', async () => {
const fields: Field[] = [
{
name: 'customField',
type: 'text',
label: 'some-collection',
},
]
await expect(async () => {
await sanitizeCollection(
// @ts-expect-error
{
...config,
collections: [
{
...collectionWithUploads,
fields,
},
],
},
{
...collectionWithUploads,
fields,
},
)
}).not.toThrow()
})
})
describe('auth -', () => {
const collectionWithAuth: CollectionConfig = {
slug: 'collection-with-auth',
fields: [],
auth: {
verify: true,
useAPIKey: true,
loginWithUsername: true,
},
}
it('should throw on hash', async () => {
const fields: Field[] = [
{
name: 'hash',
type: 'text',
label: 'some-collection',
},
]
await expect(async () => {
await sanitizeCollection(
// @ts-expect-error
{
...config,
collections: [
{
...collectionWithAuth,
fields,
},
],
},
{
...collectionWithAuth,
fields,
},
)
}).rejects.toThrow(ReservedFieldName)
})
it('should throw on salt', async () => {
const fields: Field[] = [
{
name: 'salt',
type: 'text',
label: 'some-collection',
},
]
await expect(async () => {
await sanitizeCollection(
// @ts-expect-error
{
...config,
collections: [
{
...collectionWithAuth,
fields,
},
],
},
{
...collectionWithAuth,
fields,
},
)
}).rejects.toThrow(ReservedFieldName)
})
it('should not throw on a custom field', async () => {
const fields: Field[] = [
{
name: 'customField',
type: 'text',
label: 'some-collection',
},
]
await expect(async () => {
await sanitizeCollection(
// @ts-expect-error
{
...config,
collections: [
{
...collectionWithAuth,
fields,
},
],
},
{
...collectionWithAuth,
fields,
},
)
}).not.toThrow()
})
})
})

View File

@@ -0,0 +1,150 @@
import type { Field } from '../../fields/config/types.js'
import type { CollectionConfig } from '../../index.js'
import { ReservedFieldName } from '../../errors/ReservedFieldName.js'
import { fieldAffectsData } from '../../fields/config/types.js'
// Note for future reference: We've slimmed down the reserved field names but left them in here for reference in case it's needed in the future.
/**
* Reserved field names for collections with auth config enabled
*/
const reservedBaseAuthFieldNames = [
/* 'email',
'resetPasswordToken',
'resetPasswordExpiration', */
'salt',
'hash',
]
/**
* Reserved field names for auth collections with verify: true
*/
const reservedVerifyFieldNames = [
/* '_verified', '_verificationToken' */
]
/**
* Reserved field names for auth collections with useApiKey: true
*/
const reservedAPIKeyFieldNames = [
/* 'enableAPIKey', 'apiKeyIndex', 'apiKey' */
]
/**
* Reserved field names for collections with upload config enabled
*/
const reservedBaseUploadFieldNames = [
'file',
/* 'mimeType',
'thumbnailURL',
'width',
'height',
'filesize',
'filename',
'url',
'focalX',
'focalY',
'sizes', */
]
/**
* Reserved field names for collections with versions enabled
*/
const reservedVersionsFieldNames = [
/* '__v', '_status' */
]
/**
* Sanitize fields for collections with auth config enabled.
*
* Should run on top level fields only.
*/
export const sanitizeAuthFields = (fields: Field[], config: CollectionConfig) => {
for (let i = 0; i < fields.length; i++) {
const field = fields[i]
if (fieldAffectsData(field) && field.name) {
if (config.auth && typeof config.auth === 'object' && !config.auth.disableLocalStrategy) {
const auth = config.auth
if (reservedBaseAuthFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
if (auth.verify) {
if (reservedAPIKeyFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
}
/* if (auth.maxLoginAttempts) {
if (field.name === 'loginAttempts' || field.name === 'lockUntil') {
throw new ReservedFieldName(field, field.name)
}
} */
/* if (auth.loginWithUsername) {
if (field.name === 'username') {
throw new ReservedFieldName(field, field.name)
}
} */
if (auth.verify) {
if (reservedVerifyFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
}
}
}
// Handle tabs without a name
if (field.type === 'tabs') {
for (let j = 0; j < field.tabs.length; j++) {
const tab = field.tabs[j]
if (!('name' in tab)) {
sanitizeAuthFields(tab.fields, config)
}
}
}
// Handle presentational fields like rows and collapsibles
if (!fieldAffectsData(field) && 'fields' in field && field.fields) {
sanitizeAuthFields(field.fields, config)
}
}
}
/**
* Sanitize fields for collections with upload config enabled.
*
* Should run on top level fields only.
*/
export const sanitizeUploadFields = (fields: Field[], config: CollectionConfig) => {
if (config.upload && typeof config.upload === 'object') {
for (let i = 0; i < fields.length; i++) {
const field = fields[i]
if (fieldAffectsData(field) && field.name) {
if (reservedBaseUploadFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
}
// Handle tabs without a name
if (field.type === 'tabs') {
for (let j = 0; j < field.tabs.length; j++) {
const tab = field.tabs[j]
if (!('name' in tab)) {
sanitizeUploadFields(tab.fields, config)
}
}
}
// Handle presentational fields like rows and collapsibles
if (!fieldAffectsData(field) && 'fields' in field && field.fields) {
sanitizeUploadFields(field.fields, config)
}
}
}
}

View File

@@ -14,6 +14,7 @@ import { isPlainObject } from '../../utilities/isPlainObject.js'
import baseVersionFields from '../../versions/baseFields.js'
import { versionDefaults } from '../../versions/defaults.js'
import { authDefaults, defaults, loginWithUsernameDefaults } from './defaults.js'
import { sanitizeAuthFields, sanitizeUploadFields } from './reservedFieldNames.js'
export const sanitizeCollection = async (
config: Config,
@@ -38,6 +39,7 @@ export const sanitizeCollection = async (
const validRelationships = config.collections.map((c) => c.slug) || []
sanitized.fields = await sanitizeFields({
collectionConfig: sanitized,
config,
fields: sanitized.fields,
richTextSanitizationPromises,
@@ -115,6 +117,9 @@ export const sanitizeCollection = async (
if (sanitized.upload) {
if (sanitized.upload === true) sanitized.upload = {}
// sanitize fields for reserved names
sanitizeUploadFields(sanitized.fields, sanitized)
// disable duplicate for uploads by default
sanitized.disableDuplicate = sanitized.disableDuplicate || true
@@ -133,6 +138,9 @@ export const sanitizeCollection = async (
}
if (sanitized.auth) {
// sanitize fields for reserved names
sanitizeAuthFields(sanitized.fields, sanitized)
sanitized.auth = merge(authDefaults, typeof sanitized.auth === 'object' ? sanitized.auth : {}, {
isMergeableObject: isPlainObject,
})

View File

@@ -1,5 +1,8 @@
import httpStatus from 'http-status'
// This gets dynamically reassigned during compilation
export let APIErrorName = 'APIError'
class ExtendableError<TData extends object = { [key: string]: unknown }> extends Error {
data: TData
@@ -14,6 +17,7 @@ class ExtendableError<TData extends object = { [key: string]: unknown }> extends
// show data in cause
cause: data,
})
APIErrorName = this.constructor.name
this.name = this.constructor.name
this.message = message
this.status = status

View File

@@ -5,6 +5,9 @@ import httpStatus from 'http-status'
import { APIError } from './APIError.js'
// This gets dynamically reassigned during compilation
export let ValidationErrorName = 'ValidationError'
export class ValidationError extends APIError<{
collection?: string
errors: { field: string; message: string }[]
@@ -25,5 +28,7 @@ export class ValidationError extends APIError<{
httpStatus.BAD_REQUEST,
results,
)
ValidationErrorName = this.constructor.name
}
}

View File

@@ -1,4 +1,4 @@
export { APIError } from './APIError.js'
export { APIError, APIErrorName } from './APIError.js'
export { AuthenticationError } from './AuthenticationError.js'
export { DuplicateCollection } from './DuplicateCollection.js'
export { DuplicateFieldName } from './DuplicateFieldName.js'
@@ -19,4 +19,4 @@ export { MissingFile } from './MissingFile.js'
export { NotFound } from './NotFound.js'
export { QueryError } from './QueryError.js'
export { ReservedFieldName } from './ReservedFieldName.js'
export { ValidationError } from './ValidationError.js'
export { ValidationError, ValidationErrorName } from './ValidationError.js'

View File

@@ -9,12 +9,7 @@ import type {
TextField,
} from './types.js'
import {
InvalidFieldName,
InvalidFieldRelationship,
MissingFieldType,
ReservedFieldName,
} from '../../errors/index.js'
import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors/index.js'
import { sanitizeFields } from './sanitize.js'
describe('sanitizeFields', () => {
@@ -52,23 +47,6 @@ describe('sanitizeFields', () => {
}).rejects.toThrow(InvalidFieldName)
})
it('should throw on a reserved field name', async () => {
const fields: Field[] = [
{
name: 'hash',
type: 'text',
label: 'hash',
},
]
await expect(async () => {
await sanitizeFields({
config,
fields,
validRelationships: [],
})
}).rejects.toThrow(ReservedFieldName)
})
describe('auto-labeling', () => {
it('should populate label if missing', async () => {
const fields: Field[] = [

View File

@@ -1,3 +1,4 @@
import type { CollectionConfig } from '../../collections/config/types.js'
import type { Config, SanitizedConfig } from '../../config/types.js'
import type { Field } from './types.js'
@@ -7,7 +8,6 @@ import {
InvalidFieldName,
InvalidFieldRelationship,
MissingFieldType,
ReservedFieldName,
} from '../../errors/index.js'
import { deepMerge } from '../../utilities/deepMerge.js'
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
@@ -18,6 +18,7 @@ import validations from '../validations.js'
import { fieldAffectsData, tabHasName } from './types.js'
type Args = {
collectionConfig?: CollectionConfig
config: Config
existingFieldNames?: Set<string>
fields: Field[]
@@ -40,9 +41,8 @@ type Args = {
validRelationships: null | string[]
}
export const reservedFieldNames = ['__v', 'salt', 'hash', 'file']
export const sanitizeFields = async ({
collectionConfig,
config,
existingFieldNames = new Set(),
fields,
@@ -62,11 +62,6 @@ export const sanitizeFields = async ({
throw new InvalidFieldName(field, field.name)
}
// assert that field names are not one of reserved names
if (fieldAffectsData(field) && reservedFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
// Auto-label
if (
'name' in field &&
@@ -116,10 +111,12 @@ export const sanitizeFields = async ({
}
if (field.type === 'blocks' && field.blocks) {
field.blocks = field.blocks.map((block) => ({
...block,
fields: block.fields.concat(baseBlockFields),
}))
field.blocks = field.blocks.map((block) => {
return {
...block,
fields: block.fields.concat(baseBlockFields),
}
})
}
if (field.type === 'array' && field.fields) {

View File

@@ -841,6 +841,7 @@ export type {
export type { EmailAdapter as PayloadEmailAdapter, SendEmailOptions } from './email/types.js'
export {
APIError,
APIErrorName,
AuthenticationError,
DuplicateCollection,
DuplicateFieldName,
@@ -861,6 +862,7 @@ export {
NotFound,
QueryError,
ValidationError,
ValidationErrorName,
} from './errors/index.js'
export { baseBlockFields } from './fields/baseFields/baseBlockFields.js'
export { baseIDField } from './fields/baseFields/baseIDField.js'

View File

@@ -62,6 +62,16 @@
"prepublishOnly": "pnpm clean && pnpm turbo build",
"test": "echo \"No tests available.\""
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"find-node-modules": "^2.1.3",
"range-parser": "^1.2.1"

View File

@@ -30,6 +30,16 @@
"prepublishOnly": "pnpm clean && pnpm turbo build",
"test": "jest"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@aws-sdk/client-cognito-identity": "^3.525.0",
"@aws-sdk/client-s3": "^3.525.0",

View File

@@ -46,6 +46,16 @@
"prepublishOnly": "pnpm clean && pnpm turbo build",
"test": "echo \"No tests available.\""
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@payloadcms/ui": "workspace:*",
"deepmerge": "^4.2.2",

View File

@@ -36,6 +36,16 @@
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"payload": "workspace:*"

View File

@@ -46,6 +46,16 @@
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/express": "^4.17.9",

View File

@@ -37,6 +37,16 @@
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"payload": "workspace:*"

View File

@@ -43,6 +43,16 @@
"prepublishOnly": "pnpm clean && pnpm turbo build",
"test": "echo \"Error: no tests specified\""
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@payloadcms/ui": "workspace:*",
"deepmerge": "4.3.1"

View File

@@ -37,6 +37,16 @@
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@sentry/node": "^7.55.2",
"@sentry/types": "^7.54.0",

View File

@@ -51,6 +51,16 @@
"lint:fix": "eslint --fix --ext .ts,.tsx src",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/next": "workspace:*",

View File

@@ -48,6 +48,16 @@
"lint:fix": "eslint --fix --ext .ts,.tsx src",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@payloadcms/ui": "workspace:*",
"lodash.get": "^4.4.2",

View File

@@ -40,6 +40,16 @@
"prepublishOnly": "pnpm clean && pnpm turbo build",
"translateNewKeys": "tsx scripts/translateNewKeys.ts"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@lexical/headless": "0.16.1",
"@lexical/link": "0.16.1",

View File

@@ -31,6 +31,16 @@
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"is-hotkey": "0.2.0",
"slate": "0.91.4",

View File

@@ -31,6 +31,16 @@
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@azure/abort-controller": "^1.1.0",
"@azure/storage-blob": "^12.11.0",

View File

@@ -31,6 +31,16 @@
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@google-cloud/storage": "^7.7.0",
"@payloadcms/plugin-cloud-storage": "workspace:*"

View File

@@ -31,6 +31,16 @@
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.525.0",
"@aws-sdk/lib-storage": "^3.525.0",

View File

@@ -31,6 +31,16 @@
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@payloadcms/plugin-cloud-storage": "workspace:*",
"uploadthing": "^6.10.1"

View File

@@ -31,6 +31,16 @@
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"@payloadcms/plugin-cloud-storage": "workspace:*",
"@vercel/blob": "^0.22.3"

View File

@@ -37,6 +37,16 @@
"prepublishOnly": "pnpm clean && pnpm turbo build",
"translateNewKeys": "tsx scripts/translateNewKeys/run.ts"
},
"lint-staged": {
"**/package.json": "sort-package-json",
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"date-fns": "3.3.1"
},

View File

@@ -259,7 +259,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'upload:crop',
'upload:cropToolDescription',
'upload:dragAndDrop',
'upload:addImage',
'upload:addFile',
'upload:editImage',
'upload:focalPoint',
'upload:focalPointDescription',

View File

@@ -317,7 +317,7 @@ export const arTranslations: DefaultTranslationsObject = {
within: 'في غضون',
},
upload: {
addImage: 'إضافة صورة',
addFile: 'إضافة ملف',
crop: 'محصول',
cropToolDescription: 'اسحب الزوايا المحددة للمنطقة، رسم منطقة جديدة أو قم بضبط القيم أدناه.',
dragAndDrop: 'قم بسحب وإسقاط ملفّ',

View File

@@ -320,7 +320,7 @@ export const azTranslations: DefaultTranslationsObject = {
within: 'daxilinde',
},
upload: {
addImage: 'Şəkil əlavə et',
addFile: 'Fayl əlavə et',
crop: 'Məhsul',
cropToolDescription:
'Seçilmiş sahənin köşələrini sürükləyin, yeni bir sahə çəkin və ya aşağıdakı dəyərləri düzəltin.',

View File

@@ -318,7 +318,7 @@ export const bgTranslations: DefaultTranslationsObject = {
within: 'в рамките на',
},
upload: {
addImage: 'Добавяне на изображение',
addFile: 'Добавяне на файл',
crop: 'Изрязване',
cropToolDescription:
'Плъзни ъглите на избраната област, избери нова област или коригирай стойностите по-долу.',

View File

@@ -318,7 +318,7 @@ export const csTranslations: DefaultTranslationsObject = {
within: 'uvnitř',
},
upload: {
addImage: 'Přidat obrázek',
addFile: 'Přidat soubor',
crop: 'Ořez',
cropToolDescription:
'Přetáhněte rohy vybrané oblasti, nakreslete novou oblast nebo upravte níže uvedené hodnoty.',

View File

@@ -324,7 +324,7 @@ export const deTranslations: DefaultTranslationsObject = {
within: 'innerhalb',
},
upload: {
addImage: 'Bild hinzufügen',
addFile: 'Datei hinzufügen',
crop: 'Zuschneiden',
cropToolDescription:
'Ziehen Sie die Ecken des ausgewählten Bereichs, zeichnen Sie einen neuen Bereich oder passen Sie die Werte unten an.',

View File

@@ -321,7 +321,7 @@ export const enTranslations = {
within: 'within',
},
upload: {
addImage: 'Add Image',
addFile: 'Add File',
crop: 'Crop',
cropToolDescription:
'Drag the corners of the selected area, draw a new area or adjust the values below.',

View File

@@ -323,7 +323,7 @@ export const esTranslations: DefaultTranslationsObject = {
within: 'dentro de',
},
upload: {
addImage: 'Añadir imagen',
addFile: 'Añadir archivo',
crop: 'Cultivo',
cropToolDescription:
'Arrastra las esquinas del área seleccionada, dibuja un nuevo área o ajusta los valores a continuación.',

View File

@@ -318,7 +318,7 @@ export const faTranslations: DefaultTranslationsObject = {
within: 'در داخل',
},
upload: {
addImage: 'اضافه کردن تصویر',
addFile: 'اضافه کردن فایل',
crop: 'محصول',
cropToolDescription:
'گوشه‌های منطقه انتخاب شده را بکشید، یک منطقه جدید رسم کنید یا مقادیر زیر را تنظیم کنید.',

View File

@@ -327,7 +327,7 @@ export const frTranslations: DefaultTranslationsObject = {
within: 'dans',
},
upload: {
addImage: 'Ajouter une image',
addFile: 'Ajouter un fichier',
crop: 'Recadrer',
cropToolDescription:
'Faites glisser les coins de la zone sélectionnée, dessinez une nouvelle zone ou ajustez les valeurs ci-dessous.',

View File

@@ -313,7 +313,7 @@ export const heTranslations: DefaultTranslationsObject = {
within: 'בתוך',
},
upload: {
addImage: 'הוסף תמונה',
addFile: 'הוסף קובץ',
crop: 'חתוך',
cropToolDescription: 'גרור את הפינות של האזור שנבחר, צייר אזור חדש או התאם את הערכים למטה.',
dragAndDrop: 'גרור ושחרר קובץ',

View File

@@ -319,7 +319,7 @@ export const hrTranslations: DefaultTranslationsObject = {
within: 'unutar',
},
upload: {
addImage: 'Dodaj sliku',
addFile: 'Dodaj datoteku',
crop: 'Usjev',
cropToolDescription:
'Povucite kutove odabranog područja, nacrtajte novo područje ili prilagodite vrijednosti ispod.',

View File

@@ -321,7 +321,7 @@ export const huTranslations: DefaultTranslationsObject = {
within: 'belül',
},
upload: {
addImage: 'Kép hozzáadása',
addFile: 'Fájl hozzáadása',
crop: 'Termés',
cropToolDescription:
'Húzza a kijelölt terület sarkait, rajzoljon új területet, vagy igazítsa a lentebb található értékeket.',

View File

@@ -321,7 +321,7 @@ export const itTranslations: DefaultTranslationsObject = {
within: "all'interno",
},
upload: {
addImage: 'Aggiungi immagine',
addFile: 'Aggiungi file',
crop: 'Raccolto',
cropToolDescription:
"Trascina gli angoli dell'area selezionata, disegna una nuova area o regola i valori qui sotto.",

View File

@@ -319,7 +319,7 @@ export const jaTranslations: DefaultTranslationsObject = {
within: '内で',
},
upload: {
addImage: '画像を追加',
addFile: 'ファイルを追加',
crop: 'クロップ',
cropToolDescription:
'選択したエリアのコーナーをドラッグしたり、新たなエリアを描画したり、下記の値を調整してください。',

View File

@@ -318,7 +318,7 @@ export const koTranslations: DefaultTranslationsObject = {
within: '내에서',
},
upload: {
addImage: '이미지 추가',
addFile: '파일 추가',
crop: '자르기',
cropToolDescription:
'선택한 영역의 모퉁이를 드래그하거나 새로운 영역을 그리거나 아래의 값을 조정하세요.',

View File

@@ -322,7 +322,7 @@ export const myTranslations: DefaultTranslationsObject = {
within: 'အတွင်း',
},
upload: {
addImage: 'ပုံ ထည့်ပါ',
addFile: 'ဖိုင်ထည့်ပါ',
crop: 'သုန်း',
cropToolDescription:
'ရွေးထားသည့်ဧရိယာတွင်မွေးလျှက်မှုများကိုဆွဲပြီး, အသစ်တည်ပြီးသို့မဟုတ်အောက်ပါတ',

View File

@@ -319,7 +319,7 @@ export const nbTranslations: DefaultTranslationsObject = {
within: 'innen',
},
upload: {
addImage: 'Legg til bilde',
addFile: 'Legg til fil',
crop: 'Beskjær',
cropToolDescription:
'Dra hjørnene av det valgte området, tegn et nytt område eller juster verdiene nedenfor.',

View File

@@ -321,7 +321,7 @@ export const nlTranslations: DefaultTranslationsObject = {
within: 'binnen',
},
upload: {
addImage: 'Afbeelding toevoegen',
addFile: 'Bestand toevoegen',
crop: 'Bijsnijden',
cropToolDescription:
'Sleep de hoeken van het geselecteerde gebied, teken een nieuw gebied of pas de waarden hieronder aan.',

View File

@@ -319,7 +319,7 @@ export const plTranslations: DefaultTranslationsObject = {
within: 'w ciągu',
},
upload: {
addImage: 'Dodaj obraz',
addFile: 'Dodaj plik',
crop: 'Przytnij',
cropToolDescription:
'Przeciągnij narożniki wybranego obszaru, narysuj nowy obszar lub dostosuj poniższe wartości.',

View File

@@ -320,7 +320,7 @@ export const ptTranslations: DefaultTranslationsObject = {
within: 'dentro',
},
upload: {
addImage: 'Adicionar imagem',
addFile: 'Adicionar arquivo',
crop: 'Cultura',
cropToolDescription:
'Arraste as bordas da área selecionada, desenhe uma nova área ou ajuste os valores abaixo.',

Some files were not shown because too many files have changed in this diff Show More