Compare commits
25 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8d88b8238 | ||
|
|
5aa3283dc0 | ||
|
|
45844789f2 | ||
|
|
79975f48cf | ||
|
|
bba7cf37f8 | ||
|
|
1ae71a3d24 | ||
|
|
e83eb99436 | ||
|
|
f50e599684 | ||
|
|
7dab75d85e | ||
|
|
c45fbb9149 | ||
|
|
2c16c608ba | ||
|
|
c3f6c81dc6 | ||
|
|
a7b0f8ba36 | ||
|
|
014ee1a1b2 | ||
|
|
cf6da0186b | ||
|
|
18063bd256 | ||
|
|
76b3075369 | ||
|
|
3d63ce94bb | ||
|
|
f8a5103ed7 | ||
|
|
2bd53a06eb | ||
|
|
442518dbc9 | ||
|
|
d3131122db | ||
|
|
6d0dfeafc8 | ||
|
|
00771b1f2a | ||
|
|
448186f374 |
@@ -86,19 +86,19 @@ const config = buildConfig({
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `autoLogin` | Used to automate admin log-in for dev and demonstration convenience. [More details](../authentication/overview). |
|
||||
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| `buildPath` | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| `components` | Component overrides that affect the entirety of the Admin Panel. [More details](./components). |
|
||||
| `custom` | Any custom properties you wish to pass to the Admin Panel. |
|
||||
| `dateFormat` | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| `disable` | If set to `true`, the entire Admin Panel will be disabled. |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| `meta` | Base metadata to use for the Admin Panel. Included properties are `titleSuffix`, `icons`, and `openGraph`. Can be overridden on a per Collection or per Global basis. |
|
||||
| `routes` | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| `user` | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
| Option | Description |
|
||||
|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| `autoLogin` | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
|
||||
| `buildPath` | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| `components` | Component overrides that affect the entirety of the Admin Panel. [More details](./components). |
|
||||
| `custom` | Any custom properties you wish to pass to the Admin Panel. |
|
||||
| `dateFormat` | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| `disable` | If set to `true`, the entire Admin Panel will be disabled. |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| `meta` | Base metadata to use for the Admin Panel. Included properties are `titleSuffix`, `icons`, and `openGraph`. Can be overridden on a per Collection or per Global basis. |
|
||||
| `routes` | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| `user` | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
|
||||
@@ -127,30 +127,27 @@ If set to `true`, users can log in with either their username or email address.
|
||||
|
||||
If set to `true`, an email address is required when creating a new user. If set to `false`, email is not required upon creation.
|
||||
|
||||
## Admin Auto-Login
|
||||
## Auto-Login
|
||||
|
||||
For testing and demo purposes you may want to skip forcing the admin user to login in order to access the [Admin Panel](../admin/overview). Typically, all users should be required to login to access the Admin Panel, however, you can speed up local development time by enabling auto-login.
|
||||
For testing and demo purposes you may want to skip forcing the user to login in order to access your application. Typically, all users should be required to login, however, you can speed up local development time by enabling auto-login.
|
||||
|
||||
To enable auto-login, set the `autoLogin` property in the [Admin Config](../configuration/admin):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
// ...
|
||||
// highlight-start
|
||||
autoLogin:
|
||||
process.env.NEXT_PUBLIC_ENABLE_AUTOLOGIN === 'true'
|
||||
? {
|
||||
email: 'test@example.com',
|
||||
password: 'test',
|
||||
prefillOnly: true,
|
||||
}
|
||||
: false,
|
||||
// highlight-end
|
||||
},
|
||||
// highlight-start
|
||||
autoLogin:
|
||||
process.env.NEXT_PUBLIC_ENABLE_AUTOLOGIN === 'true'
|
||||
? {
|
||||
email: 'test@example.com',
|
||||
password: 'test',
|
||||
prefillOnly: true,
|
||||
}
|
||||
: false,
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
@@ -162,9 +159,10 @@ export default buildConfig({
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
|-------------------|-----------------------------------------------------------------------------------------------------------------|
|
||||
| **`username`** | The username of the user to login as |
|
||||
| **`email`** | The email address of the user to login as |
|
||||
| **`password`** | The password of the user to login as |
|
||||
| **`password`** | The password of the user to login as. This is only needed if `prefillOnly` is set to true |
|
||||
| **`prefillOnly`** | If set to true, the login credentials will be prefilled but the user will still need to click the login button. |
|
||||
|
||||
## Operations
|
||||
|
||||
@@ -64,38 +64,38 @@ export default buildConfig({
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
|
||||
| **`bin`** | Register custom bin scripts for Payload to execute. |
|
||||
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
|
||||
| **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). |
|
||||
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
|
||||
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
||||
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
|
||||
| Option | Description |
|
||||
|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
|
||||
| **`bin`** | Register custom bin scripts for Payload to execute. |
|
||||
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
|
||||
| **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). |
|
||||
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
|
||||
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
||||
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
|
||||
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
|
||||
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
|
||||
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
|
||||
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/overview#csrf-protection). |
|
||||
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
|
||||
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
|
||||
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
|
||||
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
|
||||
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
|
||||
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
|
||||
| **`debug`** | Enable to expose more detailed error information. |
|
||||
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
|
||||
| **`rateLimit`** | Control IP-based rate limiting for all Payload resources. Used to prevent DDoS attacks, etc. [More details](../production/preventing-abuse#rate-limiting-requests). |
|
||||
| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). |
|
||||
| **`plugins`** | An array of Plugins. [More details](../plugins/overview). |
|
||||
| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||
| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). |
|
||||
| **`secret`** \* | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. |
|
||||
| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. |
|
||||
| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). |
|
||||
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
|
||||
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
|
||||
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/overview#csrf-protection). |
|
||||
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
|
||||
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
|
||||
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
|
||||
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
|
||||
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
|
||||
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
|
||||
| **`debug`** | Enable to expose more detailed error information. |
|
||||
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
|
||||
| **`rateLimit`** | Control IP-based rate limiting for all Payload resources. Used to prevent DDoS attacks, etc. [More details](../production/preventing-abuse#rate-limiting-requests). |
|
||||
| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). |
|
||||
| **`plugins`** | An array of Plugins. [More details](../plugins/overview). |
|
||||
| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||
| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). |
|
||||
| **`secret`** \* | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. |
|
||||
| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. |
|
||||
| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -375,6 +375,8 @@ export const MyCollection: CollectionConfig = {
|
||||
<Banner type="warning">
|
||||
<strong>Reminder:</strong>
|
||||
The Custom ID Fields can only be of type [`Number`](./number) or [`Text`](./text).
|
||||
|
||||
Custom ID fields with type `text` must not contain `/` or `.` characters.
|
||||
</Banner>
|
||||
|
||||
## TypeScript
|
||||
|
||||
@@ -180,7 +180,6 @@ import {
|
||||
useFormInitializing,
|
||||
useFormModified,
|
||||
useFormProcessing,
|
||||
useFormQueryParams,
|
||||
useFormSubmitted,
|
||||
useHotkey,
|
||||
useIntersect,
|
||||
@@ -221,7 +220,6 @@ import {
|
||||
EntityVisibilityProvider,
|
||||
FieldComponentsProvider,
|
||||
FieldPropsProvider,
|
||||
FormQueryParamsProvider,
|
||||
ListInfoProvider,
|
||||
ListQueryProvider,
|
||||
LocaleProvider,
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
8
examples/multi-tenant-single-domain/next.config.mjs
Normal file
8
examples/multi-tenant-single-domain/next.config.mjs
Normal 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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export default {};
|
||||
@@ -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}`)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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',
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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',
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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',
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
@@ -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',
|
||||
131
examples/multi-tenant-single-domain/src/migrations/seed.ts
Normal file
131
examples/multi-tenant-single-domain/src/migrations/seed.ts
Normal 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',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
@@ -131,4 +131,4 @@ export interface Auth {
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`)
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.68",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.68",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.68",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -34,7 +34,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bson-objectid": "2.0.4",
|
||||
"deepmerge": "4.3.1",
|
||||
"http-status": "1.6.2",
|
||||
"mongoose": "6.12.3",
|
||||
"mongoose-paginate-v2": "1.7.22",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { FilterQuery } from 'mongoose'
|
||||
import type { Field, Operator, Payload, Where } from 'payload'
|
||||
|
||||
import deepmerge from 'deepmerge'
|
||||
import { combineMerge } from 'payload'
|
||||
import { deepMergeWithCombinedArrays } from 'payload'
|
||||
import { validOperators } from 'payload/shared'
|
||||
|
||||
import { buildAndOrConditions } from './buildAndOrConditions.js'
|
||||
@@ -70,7 +69,7 @@ export async function parseParams({
|
||||
[searchParam.path]: searchParam.value,
|
||||
}
|
||||
} else if (typeof searchParam?.value === 'object') {
|
||||
result = deepmerge(result, searchParam.value, { arrayMerge: combineMerge })
|
||||
result = deepMergeWithCombinedArrays(result, searchParam.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.68",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.68",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.68",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.68",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.68",
|
||||
"description": "The official live preview React SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.68",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -14,12 +14,12 @@ async function build() {
|
||||
plugins: [sassPlugin({ css: 'external' })],
|
||||
})
|
||||
|
||||
await fs.rename('dist/prod/esbuildEntry.css', 'dist/prod/styles.css', (err) => {
|
||||
if (err) {
|
||||
console.error(`Error while renaming index.css: ${err}`)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
try {
|
||||
fs.renameSync('dist/prod/esbuildEntry.css', 'dist/prod/styles.css')
|
||||
} catch (err) {
|
||||
console.error(`Error while renaming index.css: ${err}`)
|
||||
throw err
|
||||
}
|
||||
|
||||
console.log('styles.css bundled successfully')
|
||||
|
||||
@@ -32,12 +32,12 @@ async function build() {
|
||||
]
|
||||
|
||||
for (const file of filesToDelete) {
|
||||
await fs.unlink(file, (err) => {
|
||||
if (err) {
|
||||
console.error(`Error while deleting ${file}: ${err}`)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
try {
|
||||
fs.unlinkSync(file)
|
||||
} catch (err) {
|
||||
console.error(`Error while deleting ${file}: ${err}`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Files renamed and deleted successfully')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.68",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdminViewProps, ServerSideEditViewProps } from 'payload'
|
||||
|
||||
import { DocumentInfoProvider, FormQueryParamsProvider, HydrateClientUser } from '@payloadcms/ui'
|
||||
import { DocumentInfoProvider, HydrateClientUser } from '@payloadcms/ui'
|
||||
import { RenderCustomComponent } from '@payloadcms/ui/shared'
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
@@ -65,7 +65,6 @@ export const Account: React.FC<AdminViewProps> = async ({
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} />}
|
||||
action={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
||||
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
||||
collectionSlug={userSlug}
|
||||
docPermissions={docPermissions}
|
||||
@@ -84,31 +83,22 @@ export const Account: React.FC<AdminViewProps> = async ({
|
||||
permissions={permissions}
|
||||
/>
|
||||
<HydrateClientUser permissions={permissions} user={user} />
|
||||
<FormQueryParamsProvider
|
||||
initialParams={{
|
||||
depth: 0,
|
||||
'fallback-locale': 'null',
|
||||
locale: locale?.code,
|
||||
uploadEdits: undefined,
|
||||
<RenderCustomComponent
|
||||
CustomComponent={
|
||||
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined
|
||||
}
|
||||
DefaultComponent={EditView}
|
||||
componentProps={viewComponentProps}
|
||||
serverOnlyProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
>
|
||||
<RenderCustomComponent
|
||||
CustomComponent={
|
||||
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined
|
||||
}
|
||||
DefaultComponent={EditView}
|
||||
componentProps={viewComponentProps}
|
||||
serverOnlyProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
</FormQueryParamsProvider>
|
||||
/>
|
||||
</DocumentInfoProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import type {
|
||||
AdminViewComponent,
|
||||
AdminViewProps,
|
||||
EditViewComponent,
|
||||
ServerSideEditViewProps,
|
||||
} from 'payload'
|
||||
import type { AdminViewComponent, AdminViewProps, EditViewComponent } from 'payload'
|
||||
|
||||
import {
|
||||
DocumentInfoProvider,
|
||||
EditDepthProvider,
|
||||
FormQueryParamsProvider,
|
||||
HydrateClientUser,
|
||||
} from '@payloadcms/ui'
|
||||
import { DocumentInfoProvider, EditDepthProvider, HydrateClientUser } from '@payloadcms/ui'
|
||||
import { RenderCustomComponent, isEditing as getIsEditing } from '@payloadcms/ui/shared'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
@@ -65,7 +55,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
let ErrorView: AdminViewComponent
|
||||
|
||||
let apiURL: string
|
||||
let action: string
|
||||
|
||||
const { data, formState } = await getDocumentData({
|
||||
id,
|
||||
@@ -88,8 +77,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
notFound()
|
||||
}
|
||||
|
||||
action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}`
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (collectionConfig.versions?.drafts) {
|
||||
params.append('draft', 'true')
|
||||
@@ -128,8 +115,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
notFound()
|
||||
}
|
||||
|
||||
action = `${serverURL}${apiRoute}/globals/${globalSlug}`
|
||||
|
||||
const params = new URLSearchParams({
|
||||
locale: locale?.code,
|
||||
})
|
||||
@@ -198,7 +183,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
action={action}
|
||||
apiURL={apiURL}
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
disableActions={false}
|
||||
@@ -225,34 +209,25 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
depth={1}
|
||||
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
|
||||
>
|
||||
<FormQueryParamsProvider
|
||||
initialParams={{
|
||||
depth: 0,
|
||||
'fallback-locale': 'null',
|
||||
locale: locale?.code,
|
||||
uploadEdits: undefined,
|
||||
}}
|
||||
>
|
||||
{ErrorView ? (
|
||||
<ErrorView initPageResult={initPageResult} searchParams={searchParams} />
|
||||
) : (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={ViewOverride || CustomView}
|
||||
DefaultComponent={DefaultView}
|
||||
serverOnlyProps={{
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: segments,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</FormQueryParamsProvider>
|
||||
{ErrorView ? (
|
||||
<ErrorView initPageResult={initPageResult} searchParams={searchParams} />
|
||||
) : (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={ViewOverride || CustomView}
|
||||
DefaultComponent={DefaultView}
|
||||
serverOnlyProps={{
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: segments,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EditDepthProvider>
|
||||
</DocumentInfoProvider>
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
useDocumentEvents,
|
||||
useDocumentInfo,
|
||||
useEditDepth,
|
||||
useFormQueryParams,
|
||||
useUploadEdits,
|
||||
} from '@payloadcms/ui'
|
||||
import { getFormState } from '@payloadcms/ui/shared'
|
||||
import { useRouter, useSearchParams } from 'next/navigation.js'
|
||||
@@ -58,11 +58,13 @@ export const DefaultEditView: React.FC = () => {
|
||||
const { refreshCookieAsync, user } = useAuth()
|
||||
const config = useConfig()
|
||||
const router = useRouter()
|
||||
const { dispatchFormQueryParams } = useFormQueryParams()
|
||||
const { getComponentMap, getFieldMap } = useComponentMap()
|
||||
const params = useSearchParams()
|
||||
const depth = useEditDepth()
|
||||
const params = useSearchParams()
|
||||
const { reportUpdate } = useDocumentEvents()
|
||||
const { resetUploadEdits } = useUploadEdits()
|
||||
|
||||
const locale = params.get('locale')
|
||||
|
||||
const {
|
||||
admin: { user: userSlug },
|
||||
@@ -72,8 +74,6 @@ export const DefaultEditView: React.FC = () => {
|
||||
serverURL,
|
||||
} = config
|
||||
|
||||
const locale = params.get('locale')
|
||||
|
||||
const collectionConfig =
|
||||
collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
|
||||
|
||||
@@ -130,12 +130,7 @@ export const DefaultEditView: React.FC = () => {
|
||||
const redirectRoute = `${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`
|
||||
router.push(redirectRoute)
|
||||
} else {
|
||||
dispatchFormQueryParams({
|
||||
type: 'SET',
|
||||
params: {
|
||||
uploadEdits: null,
|
||||
},
|
||||
})
|
||||
resetUploadEdits()
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -151,9 +146,9 @@ export const DefaultEditView: React.FC = () => {
|
||||
isEditing,
|
||||
refreshCookieAsync,
|
||||
adminRoute,
|
||||
locale,
|
||||
router,
|
||||
dispatchFormQueryParams,
|
||||
locale,
|
||||
resetUploadEdits,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -17,13 +17,15 @@ import { LoginField } from '../LoginField/index.js'
|
||||
import './index.scss'
|
||||
|
||||
export const LoginForm: React.FC<{
|
||||
prefillEmail?: string
|
||||
prefillPassword?: string
|
||||
prefillUsername?: string
|
||||
searchParams: { [key: string]: string | string[] | undefined }
|
||||
}> = ({ searchParams }) => {
|
||||
}> = ({ prefillEmail, prefillPassword, prefillUsername, searchParams }) => {
|
||||
const config = useConfig()
|
||||
|
||||
const {
|
||||
admin: {
|
||||
autoLogin,
|
||||
routes: { forgot: forgotRoute },
|
||||
user: userSlug,
|
||||
},
|
||||
@@ -45,27 +47,25 @@ export const LoginForm: React.FC<{
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const prefillForm = autoLogin && autoLogin.prefillOnly
|
||||
|
||||
const initialState: FormState = {
|
||||
password: {
|
||||
initialValue: prefillForm ? autoLogin.password : undefined,
|
||||
initialValue: prefillPassword ?? undefined,
|
||||
valid: true,
|
||||
value: prefillForm ? autoLogin.password : undefined,
|
||||
value: prefillPassword ?? undefined,
|
||||
},
|
||||
}
|
||||
|
||||
if (loginWithUsername) {
|
||||
initialState.username = {
|
||||
initialValue: prefillForm ? autoLogin.username : undefined,
|
||||
initialValue: prefillUsername ?? undefined,
|
||||
valid: true,
|
||||
value: prefillForm ? autoLogin.username : undefined,
|
||||
value: prefillUsername ?? undefined,
|
||||
}
|
||||
} else {
|
||||
initialState.email = {
|
||||
initialValue: prefillForm ? autoLogin.email : undefined,
|
||||
initialValue: prefillEmail ?? undefined,
|
||||
valid: true,
|
||||
value: prefillForm ? autoLogin.email : undefined,
|
||||
value: prefillEmail ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,24 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
||||
|
||||
const collectionConfig = collections.find(({ slug }) => slug === userSlug)
|
||||
|
||||
const prefillAutoLogin =
|
||||
typeof config.admin?.autoLogin === 'object' && config.admin?.autoLogin.prefillOnly
|
||||
|
||||
const prefillUsername =
|
||||
prefillAutoLogin && typeof config.admin?.autoLogin === 'object'
|
||||
? config.admin?.autoLogin.username
|
||||
: undefined
|
||||
|
||||
const prefillEmail =
|
||||
prefillAutoLogin && typeof config.admin?.autoLogin === 'object'
|
||||
? config.admin?.autoLogin.email
|
||||
: undefined
|
||||
|
||||
const prefillPassword =
|
||||
prefillAutoLogin && typeof config.admin?.autoLogin === 'object'
|
||||
? config.admin?.autoLogin.password
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={`${loginBaseClass}__brand`}>
|
||||
@@ -84,7 +102,14 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
||||
/>
|
||||
</div>
|
||||
{Array.isArray(BeforeLogins) && BeforeLogins.map((Component) => Component)}
|
||||
{!collectionConfig?.auth?.disableLocalStrategy && <LoginForm searchParams={searchParams} />}
|
||||
{!collectionConfig?.auth?.disableLocalStrategy && (
|
||||
<LoginForm
|
||||
prefillEmail={prefillEmail}
|
||||
prefillPassword={prefillPassword}
|
||||
prefillUsername={prefillUsername}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
)}
|
||||
{Array.isArray(AfterLogins) && AfterLogins.map((Component) => Component)}
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ async function build() {
|
||||
splitting: false,
|
||||
external: [
|
||||
'lodash',
|
||||
//'joi',
|
||||
'*.scss',
|
||||
'*.css',
|
||||
'@payloadcms/translations',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.68",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
@@ -84,7 +84,7 @@
|
||||
"pretest": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/env": "^15.0.0-rc.0",
|
||||
"@next/env": "^15.0.0-canary.53",
|
||||
"@payloadcms/translations": "workspace:*",
|
||||
"@swc-node/core": "1.13.1",
|
||||
"@swc-node/sourcemap-support": "0.5.0",
|
||||
@@ -99,13 +99,12 @@
|
||||
"get-tsconfig": "^4.7.2",
|
||||
"http-status": "1.6.2",
|
||||
"image-size": "^1.1.1",
|
||||
"joi": "^17.12.1",
|
||||
"json-schema-to-typescript": "11.0.3",
|
||||
"jsonwebtoken": "9.0.1",
|
||||
"minimist": "1.2.8",
|
||||
"monaco-editor": "0.38.0",
|
||||
"pino": "8.15.0",
|
||||
"pino-pretty": "10.2.0",
|
||||
"pino": "9.3.1",
|
||||
"pino-pretty": "11.2.1",
|
||||
"pluralize": "8.0.0",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"scmp": "2.1.0",
|
||||
@@ -117,7 +116,6 @@
|
||||
"@monaco-editor/react": "4.5.1",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/express-fileupload": "1.4.1",
|
||||
"@types/joi": "14.3.4",
|
||||
"@types/json-schema": "7.0.15",
|
||||
"@types/jsonwebtoken": "8.5.9",
|
||||
"@types/minimist": "1.2.2",
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { SanitizedCollectionConfig, TypeWithID } from '../collections/confi
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { Field, FieldAffectingData, RichTextField, Validate } from '../fields/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type { PayloadRequest, RequestContext } from '../types/index.js'
|
||||
import type { JsonObject, PayloadRequest, RequestContext } from '../types/index.js'
|
||||
import type { WithServerSidePropsComponentProps } from './elements/WithServerSideProps.js'
|
||||
|
||||
export type RichTextFieldProps<Value extends object, AdapterProps, ExtraFieldProperties = {}> = {
|
||||
@@ -82,7 +82,7 @@ export type BeforeChangeRichTextHookArgs<
|
||||
/**
|
||||
* The original data with locales (not modified by any hooks). Only available in `beforeChange` and `beforeDuplicate` field hooks.
|
||||
*/
|
||||
docWithLocales?: Record<string, unknown>
|
||||
docWithLocales?: JsonObject
|
||||
|
||||
duplicate?: boolean
|
||||
|
||||
@@ -98,7 +98,7 @@ export type BeforeChangeRichTextHookArgs<
|
||||
/**
|
||||
* The original siblingData with locales (not modified by any hooks).
|
||||
*/
|
||||
siblingDocWithLocales?: Record<string, unknown>
|
||||
siblingDocWithLocales?: JsonObject
|
||||
|
||||
skipValidation?: boolean
|
||||
}
|
||||
@@ -216,7 +216,7 @@ type RichTextAdapterBase<
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
showHiddenFields: boolean
|
||||
siblingDoc: Record<string, unknown>
|
||||
siblingDoc: JsonObject
|
||||
}) => void
|
||||
hooks?: RichTextHooks
|
||||
i18n?: Partial<GenericLanguages>
|
||||
|
||||
@@ -59,7 +59,7 @@ export const resetPasswordOperation = async (args: Arguments): Promise<Result> =
|
||||
collection: collectionConfig.slug,
|
||||
req,
|
||||
where: {
|
||||
resetPasswordExpiration: { greater_than: new Date() },
|
||||
resetPasswordExpiration: { greater_than: new Date().toISOString() },
|
||||
resetPasswordToken: { equals: data.token },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
import type { Where } from '../../types/index.js'
|
||||
import type { AuthStrategyFunction, User } from '../index.js'
|
||||
|
||||
import { extractJWT } from '../extractJWT.js'
|
||||
@@ -16,6 +17,46 @@ export const JWTAuthentication: AuthStrategyFunction = async ({
|
||||
}) => {
|
||||
try {
|
||||
const token = extractJWT({ headers, payload })
|
||||
|
||||
if (
|
||||
!token &&
|
||||
typeof payload?.config?.admin?.autoLogin === 'object' &&
|
||||
!payload.config.admin?.autoLogin.prefillOnly &&
|
||||
headers.get('DisableAutologin') !== 'true'
|
||||
) {
|
||||
const collection = payload.collections[payload.config.admin.user]
|
||||
|
||||
const where: Where = {
|
||||
or: [],
|
||||
}
|
||||
if (payload.config.admin?.autoLogin.email) {
|
||||
where.or.push({
|
||||
email: {
|
||||
equals: payload.config.admin?.autoLogin.email,
|
||||
},
|
||||
})
|
||||
} else if (payload.config.admin?.autoLogin.username) {
|
||||
where.or.push({
|
||||
username: {
|
||||
equals: payload.config.admin?.autoLogin.username,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const user = (
|
||||
await payload.find({
|
||||
collection: collection.config.slug,
|
||||
depth: isGraphQL ? 0 : collection.config.auth.depth,
|
||||
where,
|
||||
})
|
||||
).docs[0]
|
||||
user.collection = collection.config.slug
|
||||
user._strategy = 'local-jwt'
|
||||
return {
|
||||
user: user as User,
|
||||
}
|
||||
}
|
||||
|
||||
const decodedPayload = jwt.verify(token, payload.secret) as JWTToken & jwt.JwtPayload
|
||||
|
||||
const collection = payload.collections[decodedPayload.collection]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../../../collections/config/types.js'
|
||||
import type { Payload } from '../../../index.js'
|
||||
import type { JsonObject, Payload } from '../../../index.js'
|
||||
import type { PayloadRequest } from '../../../types/index.js'
|
||||
|
||||
type Args = {
|
||||
@@ -39,13 +39,13 @@ export const incrementLoginAttempts = async ({
|
||||
return
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = {
|
||||
const data: JsonObject = {
|
||||
loginAttempts: Number(doc.loginAttempts) + 1,
|
||||
}
|
||||
|
||||
// Lock the account if at max attempts and not already locked
|
||||
if (typeof doc.loginAttempts === 'number' && doc.loginAttempts + 1 >= maxLoginAttempts) {
|
||||
const lockUntil = new Date(Date.now() + lockTime)
|
||||
const lockUntil = new Date(Date.now() + lockTime).toISOString()
|
||||
data.lockUntil = lockUntil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { Payload } from '../../../index.js'
|
||||
import type { JsonObject, Payload } from '../../../index.js'
|
||||
import type { PayloadRequest } from '../../../types/index.js'
|
||||
|
||||
import { ValidationError } from '../../../errors/index.js'
|
||||
@@ -7,7 +7,7 @@ import { generatePasswordSaltHash } from './generatePasswordSaltHash.js'
|
||||
|
||||
type Args = {
|
||||
collection: SanitizedCollectionConfig
|
||||
doc: Record<string, unknown>
|
||||
doc: JsonObject
|
||||
password: string
|
||||
payload: Payload
|
||||
req: PayloadRequest
|
||||
|
||||
@@ -4,13 +4,13 @@ import { compile } from 'json-schema-to-typescript'
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
|
||||
import { configToJSONSchema } from '../utilities/configToJSONSchema.js'
|
||||
import Logger from '../utilities/logger.js'
|
||||
import { getLogger } from '../utilities/logger.js'
|
||||
|
||||
export async function generateTypes(
|
||||
config: SanitizedConfig,
|
||||
options?: { log: boolean },
|
||||
): Promise<void> {
|
||||
const logger = Logger()
|
||||
const logger = getLogger()
|
||||
const outputFile = process.env.PAYLOAD_TS_OUTPUT_PATH || config.typescript.outputFile
|
||||
|
||||
const shouldLog = options?.log ?? true
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { LoginWithUsernameOptions } from '../../auth/types.js'
|
||||
import type { IncomingAuthType, LoginWithUsernameOptions } from '../../auth/types.js'
|
||||
import type { CollectionConfig } from './types.js'
|
||||
|
||||
import defaultAccess from '../../auth/defaultAccess.js'
|
||||
|
||||
export const defaults = {
|
||||
export const defaults: Partial<CollectionConfig> = {
|
||||
access: {
|
||||
create: defaultAccess,
|
||||
delete: defaultAccess,
|
||||
@@ -49,7 +50,7 @@ export const defaults = {
|
||||
versions: false,
|
||||
}
|
||||
|
||||
export const authDefaults = {
|
||||
export const authDefaults: IncomingAuthType = {
|
||||
cookies: {
|
||||
sameSite: 'Lax',
|
||||
secure: false,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
150
packages/payload/src/collections/config/reservedFieldNames.ts
Normal file
150
packages/payload/src/collections/config/reservedFieldNames.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import merge from 'deepmerge'
|
||||
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type { CollectionConfig, SanitizedCollectionConfig } from './types.js'
|
||||
|
||||
@@ -9,11 +7,12 @@ import { sanitizeFields } from '../../fields/config/sanitize.js'
|
||||
import { fieldAffectsData } from '../../fields/config/types.js'
|
||||
import mergeBaseFields from '../../fields/mergeBaseFields.js'
|
||||
import { getBaseUploadFields } from '../../uploads/getBaseFields.js'
|
||||
import { deepMergeWithReactComponents } from '../../utilities/deepMerge.js'
|
||||
import { formatLabels } from '../../utilities/formatLabels.js'
|
||||
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,
|
||||
@@ -28,9 +27,7 @@ export const sanitizeCollection = async (
|
||||
// Make copy of collection config
|
||||
// /////////////////////////////////
|
||||
|
||||
const sanitized: CollectionConfig = merge(defaults, collection, {
|
||||
isMergeableObject: isPlainObject,
|
||||
})
|
||||
const sanitized: CollectionConfig = deepMergeWithReactComponents(defaults, collection)
|
||||
|
||||
// /////////////////////////////////
|
||||
// Sanitize fields
|
||||
@@ -38,6 +35,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 +113,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,9 +134,13 @@ export const sanitizeCollection = async (
|
||||
}
|
||||
|
||||
if (sanitized.auth) {
|
||||
sanitized.auth = merge(authDefaults, typeof sanitized.auth === 'object' ? sanitized.auth : {}, {
|
||||
isMergeableObject: isPlainObject,
|
||||
})
|
||||
// sanitize fields for reserved names
|
||||
sanitizeAuthFields(sanitized.fields, sanitized)
|
||||
|
||||
sanitized.auth = deepMergeWithReactComponents(
|
||||
authDefaults,
|
||||
typeof sanitized.auth === 'object' ? sanitized.auth : {},
|
||||
)
|
||||
|
||||
if (!sanitized.auth.disableLocalStrategy && sanitized.auth.verify === true) {
|
||||
sanitized.auth.verify = {}
|
||||
@@ -149,12 +154,12 @@ export const sanitizeCollection = async (
|
||||
}
|
||||
|
||||
sanitized.auth.loginWithUsername = sanitized.auth.loginWithUsername
|
||||
? merge(
|
||||
loginWithUsernameDefaults,
|
||||
typeof sanitized.auth.loginWithUsername === 'boolean'
|
||||
? {
|
||||
...loginWithUsernameDefaults,
|
||||
...(typeof sanitized.auth.loginWithUsername === 'boolean'
|
||||
? {}
|
||||
: sanitized.auth.loginWithUsername,
|
||||
)
|
||||
: sanitized.auth.loginWithUsername),
|
||||
}
|
||||
: false
|
||||
|
||||
sanitized.fields = mergeBaseFields(sanitized.fields, getBaseAuthFields(sanitized.auth))
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
import joi from 'joi'
|
||||
|
||||
import { endpointsSchema } from '../../config/schema.js'
|
||||
import {
|
||||
componentSchema,
|
||||
customViewSchema,
|
||||
livePreviewSchema,
|
||||
} from '../../config/shared/componentSchema.js'
|
||||
import { openGraphSchema } from '../../config/shared/openGraphSchema.js'
|
||||
|
||||
const collectionSchema = joi.object().keys({
|
||||
slug: joi.string().required(),
|
||||
access: joi.object({
|
||||
admin: joi.func(),
|
||||
create: joi.func(),
|
||||
delete: joi.func(),
|
||||
read: joi.func(),
|
||||
readVersions: joi.func(),
|
||||
unlock: joi.func(),
|
||||
update: joi.func(),
|
||||
}),
|
||||
admin: joi.object({
|
||||
components: joi.object({
|
||||
afterList: joi.array().items(componentSchema),
|
||||
afterListTable: joi.array().items(componentSchema),
|
||||
beforeList: joi.array().items(componentSchema),
|
||||
beforeListTable: joi.array().items(componentSchema),
|
||||
edit: joi.object({
|
||||
Description: componentSchema,
|
||||
PreviewButton: componentSchema,
|
||||
PublishButton: componentSchema,
|
||||
SaveButton: componentSchema,
|
||||
SaveDraftButton: componentSchema,
|
||||
Upload: componentSchema,
|
||||
}),
|
||||
views: joi.object({
|
||||
Edit: joi.alternatives().try(
|
||||
componentSchema,
|
||||
joi.object({
|
||||
API: joi.alternatives().try(componentSchema, customViewSchema),
|
||||
Default: joi.alternatives().try(componentSchema, customViewSchema),
|
||||
LivePreview: joi.alternatives().try(componentSchema, customViewSchema),
|
||||
Version: joi.alternatives().try(componentSchema, customViewSchema),
|
||||
Versions: joi.alternatives().try(componentSchema, customViewSchema),
|
||||
// Relationships
|
||||
// References
|
||||
}),
|
||||
),
|
||||
List: joi.alternatives().try(
|
||||
componentSchema,
|
||||
joi.object({
|
||||
Component: componentSchema,
|
||||
actions: joi.array().items(componentSchema),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
defaultColumns: joi.array().items(joi.string()),
|
||||
description: joi
|
||||
.alternatives()
|
||||
.try(joi.func(), joi.object().pattern(joi.string(), [joi.string()]), joi.string()),
|
||||
enableRichTextLink: joi.boolean(),
|
||||
enableRichTextRelationship: joi.boolean(),
|
||||
group: joi.alternatives().try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
|
||||
hidden: joi.alternatives().try(joi.boolean(), joi.func()),
|
||||
hideAPIURL: joi.bool(),
|
||||
listSearchableFields: joi.array().items(joi.string()),
|
||||
livePreview: joi.object(livePreviewSchema),
|
||||
meta: joi.object({
|
||||
description: joi.string(),
|
||||
openGraph: openGraphSchema,
|
||||
}),
|
||||
pagination: joi.object({
|
||||
defaultLimit: joi.number(),
|
||||
limits: joi.array().items(joi.number()),
|
||||
}),
|
||||
preview: joi.func(),
|
||||
useAsTitle: joi.string(),
|
||||
}),
|
||||
auth: joi.alternatives().try(
|
||||
joi.object({
|
||||
cookies: joi.object().keys({
|
||||
domain: joi.string(),
|
||||
sameSite: joi.string(), // TODO: add further specificity with joi.xor
|
||||
secure: joi.boolean(),
|
||||
}),
|
||||
depth: joi.number(),
|
||||
disableLocalStrategy: joi.boolean().valid(true),
|
||||
forgotPassword: joi.object().keys({
|
||||
generateEmailHTML: joi.func(),
|
||||
generateEmailSubject: joi.func(),
|
||||
}),
|
||||
lockTime: joi.number(),
|
||||
loginWithUsername: joi.alternatives().try(
|
||||
joi.boolean(),
|
||||
joi.object().keys({
|
||||
allowEmailLogin: joi.boolean(),
|
||||
requireEmail: joi.boolean(),
|
||||
}),
|
||||
),
|
||||
maxLoginAttempts: joi.number(),
|
||||
removeTokenFromResponses: joi.boolean().valid(true),
|
||||
strategies: joi.array().items(
|
||||
joi.object().keys({
|
||||
name: joi.string().required(),
|
||||
authenticate: joi.func().required(),
|
||||
}),
|
||||
),
|
||||
tokenExpiration: joi.number(),
|
||||
useAPIKey: joi.boolean(),
|
||||
verify: joi.alternatives().try(
|
||||
joi.boolean(),
|
||||
joi.object().keys({
|
||||
generateEmailHTML: joi.func(),
|
||||
generateEmailSubject: joi.func(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
dbName: joi.alternatives().try(joi.string(), joi.func()),
|
||||
defaultSort: joi.string(),
|
||||
disableDuplicate: joi.bool(),
|
||||
endpoints: endpointsSchema,
|
||||
fields: joi.array(),
|
||||
graphQL: joi.alternatives().try(
|
||||
joi.object().keys({
|
||||
pluralName: joi.string(),
|
||||
singularName: joi.string(),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
hooks: joi.object({
|
||||
afterChange: joi.array().items(joi.func()),
|
||||
afterDelete: joi.array().items(joi.func()),
|
||||
afterForgotPassword: joi.array().items(joi.func()),
|
||||
afterLogin: joi.array().items(joi.func()),
|
||||
afterLogout: joi.array().items(joi.func()),
|
||||
afterMe: joi.array().items(joi.func()),
|
||||
afterOperation: joi.array().items(joi.func()),
|
||||
afterRead: joi.array().items(joi.func()),
|
||||
afterRefresh: joi.array().items(joi.func()),
|
||||
beforeChange: joi.array().items(joi.func()),
|
||||
beforeDelete: joi.array().items(joi.func()),
|
||||
beforeLogin: joi.array().items(joi.func()),
|
||||
beforeOperation: joi.array().items(joi.func()),
|
||||
beforeRead: joi.array().items(joi.func()),
|
||||
beforeValidate: joi.array().items(joi.func()),
|
||||
me: joi.array().items(joi.func()),
|
||||
refresh: joi.array().items(joi.func()),
|
||||
}),
|
||||
labels: joi.object({
|
||||
plural: joi
|
||||
.alternatives()
|
||||
.try(joi.func(), joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
|
||||
singular: joi
|
||||
.alternatives()
|
||||
.try(joi.func(), joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
|
||||
}),
|
||||
timestamps: joi.boolean(),
|
||||
typescript: joi.object().keys({
|
||||
interface: joi.string(),
|
||||
}),
|
||||
upload: joi.alternatives().try(
|
||||
joi.object({
|
||||
adapter: joi.string(),
|
||||
adminThumbnail: joi.alternatives().try(joi.string(), componentSchema),
|
||||
crop: joi.bool(),
|
||||
disableLocalStorage: joi.bool(),
|
||||
externalFileHeaderFilter: joi.func(),
|
||||
filesRequiredOnCreate: joi.bool(),
|
||||
focalPoint: joi.bool(),
|
||||
formatOptions: joi.object().keys({
|
||||
format: joi.string(),
|
||||
options: joi.object(),
|
||||
}),
|
||||
handlers: joi.array().items(joi.func()),
|
||||
imageSizes: joi.array().items(
|
||||
joi
|
||||
.object()
|
||||
.keys({
|
||||
name: joi.string(),
|
||||
crop: joi.string(), // TODO: add further specificity with joi.xor
|
||||
height: joi.number().integer().allow(null),
|
||||
width: joi.number().integer().allow(null),
|
||||
})
|
||||
.unknown(),
|
||||
),
|
||||
mimeTypes: joi.array().items(joi.string()),
|
||||
modifyResponseHeaders: joi.func(),
|
||||
resizeOptions: joi
|
||||
.object()
|
||||
.keys({
|
||||
background: joi.string(),
|
||||
fastShrinkOnLoad: joi.bool(),
|
||||
fit: joi.string(),
|
||||
height: joi.number().allow(null),
|
||||
kernel: joi.string(),
|
||||
position: joi.alternatives().try(joi.string(), joi.number()),
|
||||
width: joi.number().allow(null),
|
||||
withoutEnlargement: joi.bool(),
|
||||
})
|
||||
.allow(null),
|
||||
staticDir: joi.string(),
|
||||
tempFileDir: joi.string(),
|
||||
trimOptions: joi.alternatives().try(
|
||||
joi.object().keys({
|
||||
format: joi.string(),
|
||||
options: joi.object(),
|
||||
}),
|
||||
joi.string(),
|
||||
joi.number(),
|
||||
),
|
||||
useTempFiles: joi.bool(),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
versions: joi.alternatives().try(
|
||||
joi.object({
|
||||
drafts: joi.alternatives().try(
|
||||
joi.object({
|
||||
autosave: joi.alternatives().try(
|
||||
joi.boolean(),
|
||||
joi.object({
|
||||
interval: joi.number(),
|
||||
}),
|
||||
),
|
||||
validate: joi.boolean(),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
maxPerDoc: joi.number(),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
})
|
||||
|
||||
export default collectionSchema
|
||||
@@ -28,7 +28,12 @@ import type {
|
||||
} from '../../config/types.js'
|
||||
import type { DBIdentifierName } from '../../database/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { CollectionSlug, TypedAuthOperations, TypedCollection } from '../../index.js'
|
||||
import type {
|
||||
CollectionSlug,
|
||||
JsonObject,
|
||||
TypedAuthOperations,
|
||||
TypedCollection,
|
||||
} from '../../index.js'
|
||||
import type { PayloadRequest, RequestContext } from '../../types/index.js'
|
||||
import type { SanitizedUploadConfig, UploadConfig } from '../../uploads/types.js'
|
||||
import type {
|
||||
@@ -41,7 +46,7 @@ export type DataFromCollectionSlug<TSlug extends CollectionSlug> = TypedCollecti
|
||||
export type AuthOperationsFromCollectionSlug<TSlug extends CollectionSlug> =
|
||||
TypedAuthOperations[TSlug]
|
||||
|
||||
export type RequiredDataFromCollection<TData extends Record<string, any>> = MarkOptional<
|
||||
export type RequiredDataFromCollection<TData extends JsonObject> = MarkOptional<
|
||||
TData,
|
||||
'createdAt' | 'id' | 'sizes' | 'updatedAt'
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BatchLoadFn } from 'dataloader'
|
||||
|
||||
import DataLoader from 'dataloader'
|
||||
|
||||
import type { PayloadRequest } from '../types/index.js'
|
||||
import type { JsonValue, PayloadRequest } from '../types/index.js'
|
||||
import type { TypeWithID } from './config/types.js'
|
||||
|
||||
import { isValidID } from '../utilities/isValidID.js'
|
||||
@@ -119,7 +119,7 @@ const batchAndLoadDocs =
|
||||
showHiddenFields: Boolean(showHiddenFields),
|
||||
where: {
|
||||
id: {
|
||||
in: ids,
|
||||
in: ids as JsonValue,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
import type { CollectionSlug } from '../../index.js'
|
||||
import type { CollectionSlug, JsonObject } from '../../index.js'
|
||||
import type { Document, PayloadRequest } from '../../types/index.js'
|
||||
import type {
|
||||
AfterChangeHook,
|
||||
@@ -184,7 +184,7 @@ export const createOperation = async <TSlug extends CollectionSlug>(
|
||||
// beforeChange - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
const resultWithLocales = await beforeChange<Record<string, unknown>>({
|
||||
const resultWithLocales = await beforeChange<JsonObject>({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
data,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CollectionSlug } from '../../index.js'
|
||||
import type { CollectionSlug, JsonObject, TypeWithID } from '../../index.js'
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
import type { BeforeOperationHook, Collection, DataFromCollectionSlug } from '../config/types.js'
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ export const duplicateOperation = async <TSlug extends CollectionSlug>(
|
||||
// beforeChange - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await beforeChange<DataFromCollectionSlug<TSlug>>({
|
||||
result = await beforeChange({
|
||||
id,
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
@@ -280,7 +280,7 @@ export const duplicateOperation = async <TSlug extends CollectionSlug>(
|
||||
// afterChange - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await afterChange<DataFromCollectionSlug<TSlug>>({
|
||||
result = await afterChange({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
data: versionDoc,
|
||||
|
||||
@@ -263,7 +263,7 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
|
||||
// beforeChange - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
let result = await beforeChange<DataFromCollectionSlug<TSlug>>({
|
||||
let result = await beforeChange({
|
||||
id,
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
@@ -349,7 +349,7 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
|
||||
// afterChange - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await afterChange<DataFromCollectionSlug<TSlug>>({
|
||||
result = await afterChange({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
data,
|
||||
|
||||
@@ -236,7 +236,7 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
|
||||
// beforeChange - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
let result = await beforeChange<DataFromCollectionSlug<TSlug>>({
|
||||
let result = await beforeChange({
|
||||
id,
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
@@ -341,7 +341,7 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
|
||||
// afterChange - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await afterChange<DataFromCollectionSlug<TSlug>>({
|
||||
result = await afterChange({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
data,
|
||||
|
||||
@@ -43,6 +43,27 @@ export type ClientConfig = {
|
||||
globals: ClientGlobalConfig[]
|
||||
} & Omit<SanitizedConfig, 'admin' | 'collections' | 'globals' | ServerOnlyRootProperties>
|
||||
|
||||
const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperties>[] = [
|
||||
'endpoints',
|
||||
'db',
|
||||
'editor',
|
||||
'plugins',
|
||||
'sharp',
|
||||
'onInit',
|
||||
'secret',
|
||||
'hooks',
|
||||
'bin',
|
||||
'typescript',
|
||||
'cors',
|
||||
'csrf',
|
||||
'email',
|
||||
'custom',
|
||||
'graphQL',
|
||||
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
|
||||
]
|
||||
|
||||
const serverOnlyAdminProperties: readonly Partial<ServerOnlyRootAdminProperties>[] = ['components']
|
||||
|
||||
export const createClientConfig = async ({
|
||||
config,
|
||||
t,
|
||||
@@ -53,49 +74,28 @@ export const createClientConfig = async ({
|
||||
}): Promise<ClientConfig> => {
|
||||
const clientConfig: ClientConfig = { ...config }
|
||||
|
||||
const serverOnlyConfigProperties: Partial<ServerOnlyRootProperties>[] = [
|
||||
'endpoints',
|
||||
'db',
|
||||
'editor',
|
||||
'plugins',
|
||||
'sharp',
|
||||
'onInit',
|
||||
'secret',
|
||||
'hooks',
|
||||
'bin',
|
||||
'typescript',
|
||||
'cors',
|
||||
'csrf',
|
||||
'email',
|
||||
'custom',
|
||||
'graphQL',
|
||||
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
|
||||
]
|
||||
|
||||
serverOnlyConfigProperties.forEach((key) => {
|
||||
for (const key of serverOnlyConfigProperties) {
|
||||
if (key in clientConfig) {
|
||||
delete clientConfig[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if ('localization' in clientConfig && clientConfig.localization) {
|
||||
clientConfig.localization = { ...clientConfig.localization }
|
||||
|
||||
clientConfig.localization.locales.forEach((locale) => {
|
||||
for (const locale of clientConfig.localization.locales) {
|
||||
delete locale.toString
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if ('admin' in clientConfig) {
|
||||
clientConfig.admin = { ...clientConfig.admin }
|
||||
|
||||
const serverOnlyAdminProperties: Partial<ServerOnlyRootAdminProperties>[] = ['components']
|
||||
|
||||
serverOnlyAdminProperties.forEach((key) => {
|
||||
for (const key of serverOnlyAdminProperties) {
|
||||
if (key in clientConfig.admin) {
|
||||
delete clientConfig.admin[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if ('livePreview' in clientConfig.admin) {
|
||||
clientConfig.admin.livePreview = { ...clientConfig.admin.livePreview }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AcceptedLanguages } from '@payloadcms/translations'
|
||||
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import merge from 'deepmerge'
|
||||
import { deepMergeSimple } from '@payloadcms/translations/utilities'
|
||||
|
||||
import type {
|
||||
Config,
|
||||
@@ -17,8 +17,7 @@ import { InvalidConfiguration } from '../errors/index.js'
|
||||
import { sanitizeGlobals } from '../globals/config/sanitize.js'
|
||||
import getPreferencesCollection from '../preferences/preferencesCollection.js'
|
||||
import checkDuplicateCollections from '../utilities/checkDuplicateCollections.js'
|
||||
import { deepMerge } from '../utilities/deepMerge.js'
|
||||
import { isPlainObject } from '../utilities/isPlainObject.js'
|
||||
import { deepMergeWithReactComponents } from '../utilities/deepMerge.js'
|
||||
import { defaults } from './defaults.js'
|
||||
|
||||
const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig> => {
|
||||
@@ -48,9 +47,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
|
||||
}
|
||||
|
||||
export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedConfig> => {
|
||||
const configWithDefaults: Config = merge(defaults, incomingConfig, {
|
||||
isMergeableObject: isPlainObject,
|
||||
}) as Config
|
||||
const configWithDefaults: Config = deepMergeWithReactComponents(defaults, incomingConfig)
|
||||
|
||||
if (!configWithDefaults?.serverURL) {
|
||||
configWithDefaults.serverURL = ''
|
||||
@@ -163,7 +160,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
isRoot: true,
|
||||
})
|
||||
if (config.editor.i18n && Object.keys(config.editor.i18n).length >= 0) {
|
||||
config.i18n.translations = deepMerge(config.i18n.translations, config.editor.i18n)
|
||||
config.i18n.translations = deepMergeSimple(config.i18n.translations, config.editor.i18n)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
import joi from 'joi'
|
||||
|
||||
import { adminViewSchema } from './shared/adminViewSchema.js'
|
||||
import { componentSchema, livePreviewSchema } from './shared/componentSchema.js'
|
||||
import { openGraphSchema } from './shared/openGraphSchema.js'
|
||||
|
||||
const component = joi.alternatives().try(joi.object().unknown(), joi.func())
|
||||
|
||||
export const endpointsSchema = joi.alternatives().try(
|
||||
joi.array().items(
|
||||
joi.object({
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
handler: joi.alternatives().try(joi.array().items(joi.func()), joi.func()),
|
||||
method: joi
|
||||
.string()
|
||||
.valid('get', 'head', 'post', 'put', 'patch', 'delete', 'connect', 'options'),
|
||||
path: joi.string(),
|
||||
root: joi.bool(),
|
||||
}),
|
||||
),
|
||||
joi.boolean(),
|
||||
)
|
||||
|
||||
export default joi.object({
|
||||
admin: joi.object({
|
||||
autoLogin: joi.alternatives().try(
|
||||
joi.object().keys({
|
||||
email: joi.string(),
|
||||
password: joi.string(),
|
||||
prefillOnly: joi.boolean(),
|
||||
username: joi.string(),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
avatar: joi.alternatives().try(joi.string(), component),
|
||||
buildPath: joi.string(),
|
||||
components: joi.object().keys({
|
||||
Nav: component,
|
||||
actions: joi.array().items(component),
|
||||
afterDashboard: joi.array().items(component),
|
||||
afterLogin: joi.array().items(component),
|
||||
afterNavLinks: joi.array().items(component),
|
||||
beforeDashboard: joi.array().items(component),
|
||||
beforeLogin: joi.array().items(component),
|
||||
beforeNavLinks: joi.array().items(component),
|
||||
graphics: joi.object({
|
||||
Icon: component,
|
||||
Logo: component,
|
||||
}),
|
||||
logout: joi.object({
|
||||
Button: component,
|
||||
}),
|
||||
providers: joi.array().items(component),
|
||||
views: joi.alternatives().try(
|
||||
joi.object({
|
||||
Account: joi.alternatives().try(component, adminViewSchema),
|
||||
Dashboard: joi.alternatives().try(component, adminViewSchema),
|
||||
}),
|
||||
joi.object().pattern(joi.string(), component),
|
||||
),
|
||||
}),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
dateFormat: joi.string(),
|
||||
disable: joi.bool(),
|
||||
livePreview: joi.object({
|
||||
...livePreviewSchema,
|
||||
collections: joi.array().items(joi.string()),
|
||||
globals: joi.array().items(joi.string()),
|
||||
}),
|
||||
meta: joi.object().keys({
|
||||
defaultOGImageType: joi.string().valid('off', 'dynamic', 'static'),
|
||||
description: joi.string(),
|
||||
icons: joi.array().items(
|
||||
joi.object().keys({
|
||||
type: joi.string(),
|
||||
color: joi.string(),
|
||||
fetchPriority: joi.string().valid('auto', 'high', 'low'),
|
||||
media: joi.string(),
|
||||
rel: joi.string(),
|
||||
sizes: joi.string(),
|
||||
url: joi.string(),
|
||||
}),
|
||||
),
|
||||
openGraph: openGraphSchema,
|
||||
titleSuffix: joi.string(),
|
||||
}),
|
||||
routes: joi.object({
|
||||
account: joi.string(),
|
||||
createFirstUser: joi.string(),
|
||||
forgot: joi.string(),
|
||||
inactivity: joi.string(),
|
||||
login: joi.string(),
|
||||
logout: joi.string(),
|
||||
reset: joi.string(),
|
||||
unauthorized: joi.string(),
|
||||
}),
|
||||
user: joi.string(),
|
||||
}),
|
||||
bin: joi.array().items(
|
||||
joi.object().keys({
|
||||
key: joi.string(),
|
||||
scriptPath: joi.string(),
|
||||
}),
|
||||
),
|
||||
collections: joi.array(),
|
||||
cookiePrefix: joi.string(),
|
||||
cors: [
|
||||
joi.string().valid('*'),
|
||||
joi.array().items(joi.string()),
|
||||
joi.object().keys({
|
||||
headers: joi.array().items(joi.string()),
|
||||
origins: [joi.string().valid('*'), joi.array().items(joi.string())],
|
||||
}),
|
||||
],
|
||||
csrf: joi.array().items(joi.string().allow('')).sparse(),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
db: joi.any(),
|
||||
debug: joi.boolean(),
|
||||
defaultDepth: joi.number().min(0).max(30),
|
||||
defaultMaxTextLength: joi.number(),
|
||||
editor: joi
|
||||
.object()
|
||||
.optional()
|
||||
.keys({
|
||||
CellComponent: componentSchema.optional(),
|
||||
FieldComponent: componentSchema.optional(),
|
||||
afterReadPromise: joi.func().optional(),
|
||||
outputSchema: joi.func().optional(),
|
||||
populationPromise: joi.func().optional(),
|
||||
validate: joi.func().required(),
|
||||
})
|
||||
.unknown(),
|
||||
email: joi.alternatives().try(joi.object(), joi.func()),
|
||||
endpoints: endpointsSchema,
|
||||
globals: joi.array(),
|
||||
graphQL: joi.object().keys({
|
||||
disable: joi.boolean(),
|
||||
disablePlaygroundInProduction: joi.boolean(),
|
||||
maxComplexity: joi.number(),
|
||||
mutations: joi.function(),
|
||||
queries: joi.function(),
|
||||
schemaOutputFile: joi.string(),
|
||||
}),
|
||||
hooks: joi.object().keys({
|
||||
afterError: joi.func(),
|
||||
}),
|
||||
i18n: joi.object(),
|
||||
indexSortableFields: joi.boolean(),
|
||||
local: joi.boolean(),
|
||||
localization: joi.alternatives().try(
|
||||
joi.object().keys({
|
||||
defaultLocale: joi.string(),
|
||||
fallback: joi.boolean(),
|
||||
localeCodes: joi.array().items(joi.string()),
|
||||
locales: joi.alternatives().try(
|
||||
joi.array().items(
|
||||
joi.object().keys({
|
||||
code: joi.string(),
|
||||
fallbackLocale: joi.string(),
|
||||
label: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.object().pattern(joi.string(), [joi.string()]),
|
||||
joi.string(),
|
||||
joi.valid(false),
|
||||
),
|
||||
rtl: joi.boolean(),
|
||||
toString: joi.func(),
|
||||
}),
|
||||
),
|
||||
joi.array().items(joi.string()),
|
||||
),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
maxDepth: joi.number().min(0).max(100),
|
||||
onInit: joi.func(),
|
||||
plugins: joi.array().items(joi.func()),
|
||||
routes: joi.object({
|
||||
admin: joi.string(),
|
||||
api: joi.string(),
|
||||
graphQL: joi.string(),
|
||||
graphQLPlayground: joi.string(),
|
||||
}),
|
||||
secret: joi.string(),
|
||||
serverURL: joi
|
||||
.string()
|
||||
.uri()
|
||||
.allow('')
|
||||
.custom((value, helper) => {
|
||||
const urlWithoutProtocol = value.split('//')[1]
|
||||
|
||||
if (!urlWithoutProtocol) {
|
||||
return helper.message({
|
||||
custom: 'You need to include either "https://" or "http://" in your serverURL.',
|
||||
})
|
||||
}
|
||||
|
||||
if (urlWithoutProtocol.indexOf('/') > -1) {
|
||||
return helper.message({
|
||||
custom:
|
||||
'Your serverURL cannot have a path. It can only contain a protocol, a domain, and an optional port.',
|
||||
})
|
||||
}
|
||||
|
||||
return value
|
||||
}),
|
||||
sharp: joi.any(),
|
||||
telemetry: joi.boolean(),
|
||||
typescript: joi.object({
|
||||
autoGenerate: joi.boolean(),
|
||||
declare: joi.alternatives().try(joi.boolean(), joi.object({ ignoreTSError: joi.boolean() })),
|
||||
outputFile: joi.string(),
|
||||
schema: joi.array().items(joi.func()),
|
||||
}),
|
||||
upload: joi.object(),
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import joi from 'joi'
|
||||
|
||||
import { componentSchema } from './componentSchema.js'
|
||||
|
||||
export const adminViewSchema = joi.array().items(
|
||||
joi.object().keys({
|
||||
Component: componentSchema,
|
||||
exact: joi.bool(),
|
||||
path: joi.string().required(),
|
||||
sensitive: joi.bool(),
|
||||
strict: joi.bool(),
|
||||
}),
|
||||
)
|
||||
@@ -1,31 +0,0 @@
|
||||
import joi from 'joi'
|
||||
|
||||
export const componentSchema = joi.alternatives().try(joi.object().unknown(), joi.func())
|
||||
|
||||
export const documentTabSchema = {
|
||||
condition: joi.func(),
|
||||
href: joi.alternatives().try(joi.string(), joi.func()).required(),
|
||||
isActive: joi.alternatives().try(joi.func(), joi.boolean()),
|
||||
label: joi.alternatives().try(joi.string(), joi.func()).required(),
|
||||
newTab: joi.boolean(),
|
||||
pillLabel: joi.alternatives().try(joi.string(), joi.func()),
|
||||
}
|
||||
|
||||
export const customViewSchema = joi.object({
|
||||
Component: componentSchema,
|
||||
Tab: joi.alternatives().try(documentTabSchema, componentSchema),
|
||||
actions: joi.array().items(componentSchema),
|
||||
path: joi.string(),
|
||||
})
|
||||
|
||||
export const livePreviewSchema = {
|
||||
breakpoints: joi.array().items(
|
||||
joi.object({
|
||||
name: joi.string(),
|
||||
height: joi.alternatives().try(joi.number(), joi.string()),
|
||||
label: joi.string(),
|
||||
width: joi.alternatives().try(joi.number(), joi.string()),
|
||||
}),
|
||||
),
|
||||
url: joi.alternatives().try(joi.string(), joi.func()),
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import joi from 'joi'
|
||||
|
||||
const ogImageObj = joi.object({
|
||||
type: joi.string(),
|
||||
alt: joi.string(),
|
||||
height: joi.alternatives().try(joi.string(), joi.number()),
|
||||
url: joi.string(),
|
||||
width: joi.alternatives().try(joi.string(), joi.number()),
|
||||
})
|
||||
|
||||
export const openGraphSchema = joi.object({
|
||||
description: joi.string(),
|
||||
images: joi.alternatives().try(ogImageObj, joi.array().items(ogImageObj)),
|
||||
siteName: joi.string(),
|
||||
title: joi.string(),
|
||||
})
|
||||
@@ -453,16 +453,15 @@ export type CORSConfig = {
|
||||
export type Config = {
|
||||
/** Configure admin dashboard */
|
||||
admin?: {
|
||||
/** Automatically log in as a user when visiting the admin dashboard. */
|
||||
/** Automatically log in as a user */
|
||||
autoLogin?:
|
||||
| {
|
||||
/**
|
||||
* The email address of the user to login as
|
||||
*
|
||||
*/
|
||||
email?: string
|
||||
/** The password of the user to login as */
|
||||
password: string
|
||||
/** The password of the user to login as. This is only needed if `prefillOnly` is set to true */
|
||||
password?: string
|
||||
/**
|
||||
* If set to true, the login credentials will be prefilled but the user will still need to click the login button.
|
||||
*
|
||||
@@ -473,9 +472,9 @@ export type Config = {
|
||||
username?: string
|
||||
}
|
||||
| false
|
||||
|
||||
/** Set account profile picture. Options: gravatar, default or a custom React component. */
|
||||
avatar?: 'default' | 'gravatar' | React.ComponentType<any>
|
||||
|
||||
/**
|
||||
* Add extra and/or replace built-in components with custom components
|
||||
*
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import type { ValidationResult } from 'joi'
|
||||
import type { Logger } from 'pino'
|
||||
|
||||
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type { SanitizedConfig } from './types.js'
|
||||
|
||||
import collectionSchema from '../collections/config/schema.js'
|
||||
import fieldSchema, { idField } from '../fields/config/schema.js'
|
||||
import { fieldAffectsData } from '../fields/config/types.js'
|
||||
import globalSchema from '../globals/config/schema.js'
|
||||
import schema from './schema.js'
|
||||
|
||||
const validateFields = (
|
||||
context: string,
|
||||
entity: SanitizedCollectionConfig | SanitizedGlobalConfig,
|
||||
): string[] => {
|
||||
const errors: string[] = []
|
||||
entity.fields.forEach((field) => {
|
||||
let idResult: Partial<ValidationResult> = { error: null }
|
||||
if (fieldAffectsData(field) && field.name === 'id') {
|
||||
idResult = idField.validate(field, { abortEarly: false })
|
||||
}
|
||||
|
||||
const result = fieldSchema.validate(field, { abortEarly: false })
|
||||
if (idResult.error) {
|
||||
idResult.error.details.forEach(({ message }) => {
|
||||
errors.push(
|
||||
`${context} "${entity.slug}" > Field${
|
||||
fieldAffectsData(field) ? ` "${field.name}" >` : ''
|
||||
} ${message}`,
|
||||
)
|
||||
})
|
||||
}
|
||||
if (result.error) {
|
||||
result.error.details.forEach(({ message }) => {
|
||||
errors.push(
|
||||
`${context} "${entity.slug}" > Field${
|
||||
fieldAffectsData(field) ? ` "${field.name}" >` : ''
|
||||
} ${message}`,
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
return errors
|
||||
}
|
||||
|
||||
const validateCollections = (collections: SanitizedCollectionConfig[]): string[] => {
|
||||
const errors: string[] = []
|
||||
collections.forEach((collection) => {
|
||||
const result = collectionSchema.validate(collection, { abortEarly: false })
|
||||
if (result.error) {
|
||||
result.error.details.forEach(({ message }) => {
|
||||
errors.push(`Collection "${collection.slug}" > ${message}`)
|
||||
})
|
||||
}
|
||||
errors.push(...validateFields('Collection', collection))
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const validateGlobals = (globals: SanitizedGlobalConfig[]): string[] => {
|
||||
const errors: string[] = []
|
||||
globals.forEach((global) => {
|
||||
const result = globalSchema.validate(global, { abortEarly: false })
|
||||
if (result.error) {
|
||||
result.error.details.forEach(({ message }) => {
|
||||
errors.push(`Globals "${global.slug}" > ${message}`)
|
||||
})
|
||||
}
|
||||
errors.push(...validateFields('Global', global))
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
export const validateSchema = (config: SanitizedConfig, logger: Logger): SanitizedConfig => {
|
||||
const result = schema.validate(config, {
|
||||
abortEarly: false,
|
||||
})
|
||||
|
||||
const nestedErrors = [
|
||||
...validateCollections(config.collections),
|
||||
...validateGlobals(config.globals),
|
||||
]
|
||||
|
||||
if (result.error || nestedErrors.length > 0) {
|
||||
logger.error(
|
||||
`There were ${
|
||||
(result.error?.details?.length || 0) + nestedErrors.length
|
||||
} errors validating your Payload config`,
|
||||
)
|
||||
|
||||
let i = 0
|
||||
if (result.error) {
|
||||
result.error.details.forEach(({ message }) => {
|
||||
i += 1
|
||||
logger.error(`${i}: ${message}`)
|
||||
})
|
||||
}
|
||||
nestedErrors.forEach((message) => {
|
||||
i += 1
|
||||
logger.error(`${i}: ${message}`)
|
||||
})
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
return result.value
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -24,10 +24,18 @@ export { formatFilesize } from '../uploads/formatFilesize.js'
|
||||
|
||||
export { isImage } from '../uploads/isImage.js'
|
||||
|
||||
export { deepCopyObject } from '../utilities/deepCopyObject.js'
|
||||
|
||||
export { deepMerge } from '../utilities/deepMerge.js'
|
||||
export {
|
||||
deepCopyObject,
|
||||
deepCopyObjectComplex,
|
||||
deepCopyObjectSimple,
|
||||
} from '../utilities/deepCopyObject.js'
|
||||
|
||||
export {
|
||||
deepMerge,
|
||||
deepMergeWithCombinedArrays,
|
||||
deepMergeWithReactComponents,
|
||||
deepMergeWithSourceArrays,
|
||||
} from '../utilities/deepMerge.js'
|
||||
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
|
||||
|
||||
export { getDataByPath } from '../utilities/getDataByPath.js'
|
||||
@@ -55,3 +63,5 @@ export { wait } from '../utilities/wait.js'
|
||||
export { default as wordBoundariesRegex } from '../utilities/wordBoundariesRegex.js'
|
||||
|
||||
export { versionDefaults } from '../versions/defaults.js'
|
||||
|
||||
export { deepMergeSimple } from '@payloadcms/translations/utilities'
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { deepMergeSimple } from '@payloadcms/translations/utilities'
|
||||
|
||||
import type { CollectionConfig } from '../../collections/config/types.js'
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type { Field } from './types.js'
|
||||
|
||||
@@ -7,9 +10,7 @@ import {
|
||||
InvalidFieldName,
|
||||
InvalidFieldRelationship,
|
||||
MissingFieldType,
|
||||
ReservedFieldName,
|
||||
} from '../../errors/index.js'
|
||||
import { deepMerge } from '../../utilities/deepMerge.js'
|
||||
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
|
||||
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
|
||||
import { baseIDField } from '../baseFields/baseIDField.js'
|
||||
@@ -18,6 +19,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 +42,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 +63,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 +112,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) {
|
||||
@@ -178,7 +176,7 @@ export const sanitizeFields = async ({
|
||||
}
|
||||
|
||||
if (field.editor.i18n && Object.keys(field.editor.i18n).length >= 0) {
|
||||
config.i18n.translations = deepMerge(config.i18n.translations, field.editor.i18n)
|
||||
config.i18n.translations = deepMergeSimple(config.i18n.translations, field.editor.i18n)
|
||||
}
|
||||
}
|
||||
if (richTextSanitizationPromises) {
|
||||
|
||||
@@ -1,592 +0,0 @@
|
||||
import joi from 'joi'
|
||||
|
||||
import { componentSchema } from '../../config/shared/componentSchema.js'
|
||||
|
||||
export const baseAdminComponentFields = joi
|
||||
.object()
|
||||
.keys({
|
||||
Cell: componentSchema,
|
||||
Description: componentSchema,
|
||||
Field: componentSchema,
|
||||
Filter: componentSchema,
|
||||
})
|
||||
.default({})
|
||||
|
||||
export const baseAdminFields = joi.object().keys({
|
||||
className: joi.string(),
|
||||
components: baseAdminComponentFields,
|
||||
condition: joi.func(),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
description: joi
|
||||
.alternatives()
|
||||
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()]), joi.function()),
|
||||
disableBulkEdit: joi.boolean().default(false),
|
||||
disableListColumn: joi.boolean().default(false),
|
||||
disableListFilter: joi.boolean().default(false),
|
||||
disabled: joi.boolean().default(false),
|
||||
hidden: joi.boolean().default(false),
|
||||
initCollapsed: joi.boolean().default(false),
|
||||
position: joi.string().valid('sidebar'),
|
||||
readOnly: joi.boolean().default(false),
|
||||
style: joi.object().unknown(),
|
||||
width: joi.string(),
|
||||
})
|
||||
|
||||
export const baseField = joi
|
||||
.object()
|
||||
.keys({
|
||||
access: joi.object().keys({
|
||||
create: joi.func(),
|
||||
read: joi.func(),
|
||||
update: joi.func(),
|
||||
}),
|
||||
admin: baseAdminFields.default(),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
hidden: joi.boolean().default(false),
|
||||
hooks: joi
|
||||
.object()
|
||||
.keys({
|
||||
afterChange: joi.array().items(joi.func()).default([]),
|
||||
afterRead: joi.array().items(joi.func()).default([]),
|
||||
beforeChange: joi.array().items(joi.func()).default([]),
|
||||
beforeDuplicate: joi.array().items(joi.func()).default([]),
|
||||
beforeValidate: joi.array().items(joi.func()).default([]),
|
||||
})
|
||||
.default(),
|
||||
index: joi.boolean().default(false),
|
||||
label: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.func(),
|
||||
joi.object().pattern(joi.string(), [joi.string()]),
|
||||
joi.string(),
|
||||
joi.valid(false),
|
||||
),
|
||||
localized: joi.boolean().default(false),
|
||||
required: joi.boolean().default(false),
|
||||
saveToJWT: joi.alternatives().try(joi.boolean(), joi.string()).default(false),
|
||||
typescriptSchema: joi.array().items(joi.func()),
|
||||
unique: joi.boolean().default(false),
|
||||
validate: joi.func(),
|
||||
})
|
||||
.default()
|
||||
|
||||
export const idField = baseField.keys({
|
||||
name: joi.string().valid('id'),
|
||||
type: joi.string().valid('text', 'number'),
|
||||
localized: joi.invalid(true),
|
||||
required: joi.not(false, 0).default(true),
|
||||
})
|
||||
|
||||
export const text = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('text').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
autoComplete: joi.string(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
placeholder: joi
|
||||
.alternatives()
|
||||
.try(joi.object().pattern(joi.string(), [joi.string()]), joi.string()),
|
||||
rtl: joi.boolean(),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
|
||||
hasMany: joi.boolean().default(false),
|
||||
maxLength: joi.number(),
|
||||
maxRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
|
||||
minLength: joi.number(),
|
||||
minRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
|
||||
})
|
||||
|
||||
export const number = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('number').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
autoComplete: joi.string(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi
|
||||
.array()
|
||||
.items(componentSchema)
|
||||
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
|
||||
beforeInput: joi
|
||||
.array()
|
||||
.items(componentSchema)
|
||||
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
step: joi.number(),
|
||||
}),
|
||||
defaultValue: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.number(),
|
||||
joi.func(),
|
||||
joi.array().when('hasMany', { not: true, then: joi.forbidden() }),
|
||||
),
|
||||
hasMany: joi.boolean().default(false),
|
||||
max: joi.number(),
|
||||
maxRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
|
||||
min: joi.number(),
|
||||
minRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
|
||||
})
|
||||
|
||||
export const textarea = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('textarea').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
rows: joi.number(),
|
||||
rtl: joi.boolean(),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
|
||||
maxLength: joi.number(),
|
||||
minLength: joi.number(),
|
||||
})
|
||||
|
||||
export const email = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('email').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
autoComplete: joi.string(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
|
||||
maxLength: joi.number(),
|
||||
minLength: joi.number(),
|
||||
})
|
||||
|
||||
export const code = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('code').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
editorOptions: joi.object().unknown(), // Editor['options'] @monaco-editor/react
|
||||
language: joi.string(),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
|
||||
})
|
||||
|
||||
export const json = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('json').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
editorOptions: joi.object().unknown(), // Editor['options'] @monaco-editor/react
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.array(), joi.func(), joi.object()),
|
||||
jsonSchema: joi.object().unknown(),
|
||||
})
|
||||
|
||||
export const select = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('select').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
isClearable: joi.boolean().default(false),
|
||||
isSortable: joi.boolean().default(false),
|
||||
}),
|
||||
dbName: joi.alternatives().try(joi.string(), joi.func()),
|
||||
defaultValue: joi
|
||||
.alternatives()
|
||||
.try(joi.string().allow(''), joi.array().items(joi.string().allow('')), joi.func()),
|
||||
enumName: joi.alternatives().try(joi.string(), joi.func()),
|
||||
hasMany: joi.boolean().default(false),
|
||||
options: joi
|
||||
.array()
|
||||
.min(1)
|
||||
.items(
|
||||
joi.alternatives().try(
|
||||
joi.string(),
|
||||
joi.object({
|
||||
label: joi
|
||||
.alternatives()
|
||||
.try(joi.func(), joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
|
||||
value: joi.string().required().allow(''),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.required(),
|
||||
})
|
||||
|
||||
export const radio = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('radio').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
layout: joi.string().valid('vertical', 'horizontal'),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
|
||||
enumName: joi.alternatives().try(joi.string(), joi.func()),
|
||||
options: joi
|
||||
.array()
|
||||
.min(1)
|
||||
.items(
|
||||
joi.alternatives().try(
|
||||
joi.string(),
|
||||
joi.object({
|
||||
label: joi
|
||||
.alternatives()
|
||||
.try(joi.func(), joi.string(), joi.object().pattern(joi.string(), [joi.string()]))
|
||||
.required(),
|
||||
value: joi.string().required().allow(''),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.required(),
|
||||
})
|
||||
|
||||
export const row = baseField.keys({
|
||||
type: joi.string().valid('row').required(),
|
||||
admin: baseAdminFields.default(),
|
||||
fields: joi.array().items(joi.link('#field')),
|
||||
})
|
||||
|
||||
export const collapsible = baseField.keys({
|
||||
type: joi.string().valid('collapsible').required(),
|
||||
admin: baseAdminFields
|
||||
.keys({
|
||||
components: baseAdminComponentFields
|
||||
.keys({
|
||||
RowLabel: componentSchema.optional(),
|
||||
})
|
||||
.default({}),
|
||||
})
|
||||
.default({}),
|
||||
fields: joi.array().items(joi.link('#field')),
|
||||
label: joi.alternatives().conditional('admin.components.RowLabel', {
|
||||
is: joi.exist(),
|
||||
otherwise: joi
|
||||
.alternatives()
|
||||
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()]), joi.function())
|
||||
.required(),
|
||||
then: joi
|
||||
.alternatives()
|
||||
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()]), joi.function())
|
||||
.optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
const tab = baseField.keys({
|
||||
name: joi.string().when('localized', { is: joi.exist(), then: joi.required() }),
|
||||
description: joi.alternatives().try(joi.string(), componentSchema),
|
||||
fields: joi.array().items(joi.link('#field')).required(),
|
||||
interfaceName: joi.string().when('name', { not: joi.exist(), then: joi.forbidden() }),
|
||||
label: joi
|
||||
.alternatives()
|
||||
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()]))
|
||||
.when('name', { is: joi.not(), then: joi.required() }),
|
||||
localized: joi.boolean(),
|
||||
saveToJWT: joi.alternatives().try(joi.boolean(), joi.string()),
|
||||
})
|
||||
|
||||
export const tabs = baseField.keys({
|
||||
type: joi.string().valid('tabs').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
description: joi.forbidden(),
|
||||
}),
|
||||
fields: joi.forbidden(),
|
||||
localized: joi.forbidden(),
|
||||
tabs: joi.array().items(tab).required(),
|
||||
})
|
||||
|
||||
export const group = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('group').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
hideGutter: joi.boolean().default(true),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.object(), joi.func()),
|
||||
fields: joi.array().items(joi.link('#field')),
|
||||
interfaceName: joi.string(),
|
||||
})
|
||||
|
||||
export const array = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('array').required(),
|
||||
admin: baseAdminFields
|
||||
.keys({
|
||||
components: baseAdminComponentFields
|
||||
.keys({
|
||||
RowLabel: componentSchema,
|
||||
})
|
||||
.default({}),
|
||||
isSortable: joi.boolean(),
|
||||
})
|
||||
.default({}),
|
||||
dbName: joi.alternatives().try(joi.string(), joi.func()),
|
||||
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()),
|
||||
fields: joi.array().items(joi.link('#field')).required(),
|
||||
interfaceName: joi.string(),
|
||||
labels: joi.object({
|
||||
plural: joi
|
||||
.alternatives()
|
||||
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
|
||||
singular: joi
|
||||
.alternatives()
|
||||
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
|
||||
}),
|
||||
maxRows: joi.number(),
|
||||
minRows: joi.number(),
|
||||
})
|
||||
|
||||
export const upload = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('upload').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.object(), joi.func()),
|
||||
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
|
||||
maxDepth: joi.number(),
|
||||
relationTo: joi.string().required(),
|
||||
})
|
||||
|
||||
export const checkbox = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('checkbox').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.boolean(), joi.func()),
|
||||
})
|
||||
|
||||
export const point = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('point').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.array().items(joi.number()).max(2).min(2), joi.func()),
|
||||
})
|
||||
|
||||
export const relationship = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('relationship').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
allowCreate: joi.boolean().default(true),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
isSortable: joi.boolean().default(false),
|
||||
sortOptions: joi.alternatives().conditional(joi.ref('...relationTo'), {
|
||||
is: joi.string(),
|
||||
otherwise: joi.object().pattern(joi.string(), joi.string()),
|
||||
then: joi.string(),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.func()),
|
||||
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
|
||||
hasMany: joi.boolean().default(false),
|
||||
max: joi
|
||||
.number()
|
||||
.when('hasMany', { is: joi.not(true), then: joi.forbidden() })
|
||||
.warning('deprecated', { message: 'Use maxRows instead.' }),
|
||||
maxDepth: joi.number(),
|
||||
maxRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
|
||||
min: joi
|
||||
.number()
|
||||
.when('hasMany', { is: joi.not(true), then: joi.forbidden() })
|
||||
.warning('deprecated', { message: 'Use minRows instead.' }),
|
||||
minRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
|
||||
relationTo: joi.alternatives().try(joi.string().required(), joi.array().items(joi.string())),
|
||||
})
|
||||
|
||||
export const blocks = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('blocks').required(),
|
||||
admin: baseAdminFields
|
||||
.keys({
|
||||
isSortable: joi.boolean(),
|
||||
})
|
||||
.default({}),
|
||||
blocks: joi
|
||||
.array()
|
||||
.items(
|
||||
joi.object({
|
||||
slug: joi.string().required(),
|
||||
admin: joi.object().keys({
|
||||
components: joi.object().keys({
|
||||
Label: componentSchema,
|
||||
}),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
}),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
dbName: joi.alternatives().try(joi.string(), joi.func()),
|
||||
fields: joi.array().items(joi.link('#field')),
|
||||
graphQL: joi.object().keys({
|
||||
singularName: joi.string(),
|
||||
}),
|
||||
imageAltText: joi.string(),
|
||||
imageURL: joi.string(),
|
||||
interfaceName: joi.string(),
|
||||
labels: joi.object({
|
||||
plural: joi
|
||||
.alternatives()
|
||||
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
|
||||
singular: joi
|
||||
.alternatives()
|
||||
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.required(),
|
||||
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()),
|
||||
labels: joi.object({
|
||||
plural: joi
|
||||
.alternatives()
|
||||
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
|
||||
singular: joi
|
||||
.alternatives()
|
||||
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
|
||||
}),
|
||||
maxRows: joi.number(),
|
||||
minRows: joi.number(),
|
||||
})
|
||||
|
||||
export const richText = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('richText').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func(), joi.object()),
|
||||
editor: joi
|
||||
.object()
|
||||
.keys({
|
||||
CellComponent: componentSchema.optional(),
|
||||
FieldComponent: componentSchema.optional(),
|
||||
afterReadPromise: joi.func().optional(),
|
||||
graphQLPopulationPromises: joi.func().optional(),
|
||||
outputSchema: joi.func().optional(),
|
||||
validate: joi.func().required(),
|
||||
})
|
||||
.unknown(),
|
||||
maxDepth: joi.number(),
|
||||
})
|
||||
|
||||
export const date = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('date').required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
date: joi.object({
|
||||
displayFormat: joi.string(),
|
||||
maxDate: joi.date(),
|
||||
maxTime: joi.date(),
|
||||
minDate: joi.date(),
|
||||
minTime: joi.date(),
|
||||
monthsToShow: joi.number(),
|
||||
overrides: joi.object().unknown(),
|
||||
pickerAppearance: joi.string(),
|
||||
timeFormat: joi.string(),
|
||||
timeIntervals: joi.number(),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
|
||||
})
|
||||
|
||||
export const ui = joi.object().keys({
|
||||
name: joi.string().required(),
|
||||
type: joi.string().valid('ui').required(),
|
||||
admin: joi
|
||||
.object()
|
||||
.keys({
|
||||
components: joi
|
||||
.object()
|
||||
.keys({
|
||||
Cell: componentSchema,
|
||||
Field: componentSchema,
|
||||
})
|
||||
.default({}),
|
||||
condition: joi.func(),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
disableListColumn: joi.boolean().default(false),
|
||||
position: joi.string().valid('sidebar'),
|
||||
width: joi.string(),
|
||||
})
|
||||
.default(),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
label: joi.alternatives().try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
|
||||
})
|
||||
|
||||
const fieldSchema = joi
|
||||
.alternatives()
|
||||
.try(
|
||||
text,
|
||||
number,
|
||||
textarea,
|
||||
email,
|
||||
code,
|
||||
json,
|
||||
select,
|
||||
group,
|
||||
array,
|
||||
row,
|
||||
collapsible,
|
||||
tabs,
|
||||
radio,
|
||||
relationship,
|
||||
checkbox,
|
||||
upload,
|
||||
richText,
|
||||
blocks,
|
||||
date,
|
||||
point,
|
||||
ui,
|
||||
)
|
||||
.id('field')
|
||||
|
||||
export default fieldSchema
|
||||
@@ -1,29 +1,31 @@
|
||||
import type { PayloadRequest } from '../types/index.js'
|
||||
import type { JsonValue, PayloadRequest } from '../types/index.js'
|
||||
|
||||
import { deepCopyObject } from '../utilities/deepCopyObject.js'
|
||||
import { deepCopyObjectSimple } from '../utilities/deepCopyObject.js'
|
||||
|
||||
type Args = {
|
||||
defaultValue: unknown
|
||||
defaultValue: ((args: any) => JsonValue) | any
|
||||
locale: string | undefined
|
||||
user: PayloadRequest['user']
|
||||
value?: unknown
|
||||
value?: JsonValue
|
||||
}
|
||||
|
||||
const getValueWithDefault = ({ defaultValue, locale, user, value }: Args): unknown => {
|
||||
export const getDefaultValue = async ({
|
||||
defaultValue,
|
||||
locale,
|
||||
user,
|
||||
value,
|
||||
}: Args): Promise<JsonValue> => {
|
||||
if (typeof value !== 'undefined') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (defaultValue && typeof defaultValue === 'function') {
|
||||
return defaultValue({ locale, user })
|
||||
return await defaultValue({ locale, user })
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'object') {
|
||||
return deepCopyObject(defaultValue)
|
||||
return deepCopyObjectSimple(defaultValue)
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default getValueWithDefault
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
|
||||
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
|
||||
|
||||
import { deepCopyObject } from '../../../utilities/deepCopyObject.js'
|
||||
import { deepCopyObjectSimple } from '../../../utilities/deepCopyObject.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args<T> = {
|
||||
type Args<T extends JsonObject> = {
|
||||
collection: SanitizedCollectionConfig | null
|
||||
context: RequestContext
|
||||
/**
|
||||
* The data before hooks
|
||||
*/
|
||||
data: Record<string, unknown> | T
|
||||
data: T
|
||||
/**
|
||||
* The data after hooks
|
||||
*/
|
||||
doc: Record<string, unknown> | T
|
||||
doc: T
|
||||
global: SanitizedGlobalConfig | null
|
||||
operation: 'create' | 'update'
|
||||
previousDoc: Record<string, unknown> | T
|
||||
previousDoc: T
|
||||
req: PayloadRequest
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type Args<T> = {
|
||||
* This function is responsible for the following actions, in order:
|
||||
* - Execute field hooks
|
||||
*/
|
||||
export const afterChange = async <T extends Record<string, unknown>>({
|
||||
export const afterChange = async <T extends JsonObject>({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -36,7 +36,7 @@ export const afterChange = async <T extends Record<string, unknown>>({
|
||||
previousDoc,
|
||||
req,
|
||||
}: Args<T>): Promise<T> => {
|
||||
const doc = deepCopyObject(incomingDoc)
|
||||
const doc = deepCopyObjectSimple(incomingDoc)
|
||||
|
||||
await traverseFields({
|
||||
collection,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
|
||||
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
@@ -12,8 +12,8 @@ import { traverseFields } from './traverseFields.js'
|
||||
type Args = {
|
||||
collection: SanitizedCollectionConfig | null
|
||||
context: RequestContext
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
data: JsonObject
|
||||
doc: JsonObject
|
||||
field: Field | TabAsField
|
||||
global: SanitizedGlobalConfig | null
|
||||
operation: 'create' | 'update'
|
||||
@@ -25,11 +25,11 @@ type Args = {
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
previousDoc: Record<string, unknown>
|
||||
previousSiblingDoc: Record<string, unknown>
|
||||
previousDoc: JsonObject
|
||||
previousSiblingDoc: JsonObject
|
||||
req: PayloadRequest
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
siblingData: JsonObject
|
||||
siblingDoc: JsonObject
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
@@ -101,11 +101,11 @@ export const promise = async ({
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc[field.name] as Record<string, unknown>,
|
||||
previousSiblingDoc: previousDoc[field.name] as JsonObject,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: (siblingData?.[field.name] as Record<string, unknown>) || {},
|
||||
siblingDoc: siblingDoc[field.name] as Record<string, unknown>,
|
||||
siblingData: (siblingData?.[field.name] as JsonObject) || {},
|
||||
siblingDoc: siblingDoc[field.name] as JsonObject,
|
||||
})
|
||||
|
||||
break
|
||||
@@ -128,11 +128,11 @@ export const promise = async ({
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject),
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||
siblingDoc: { ...row } || {},
|
||||
siblingDoc: ({ ...(row as JsonObject) } as JsonObject) || {},
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -147,7 +147,9 @@ export const promise = async ({
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find((blockType) => blockType.slug === row.blockType)
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
|
||||
if (block) {
|
||||
promises.push(
|
||||
@@ -161,12 +163,11 @@ export const promise = async ({
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
previousDoc,
|
||||
previousSiblingDoc:
|
||||
previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject),
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||
siblingDoc: { ...row } || {},
|
||||
siblingDoc: ({ ...(row as JsonObject) } as JsonObject) || {},
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -205,9 +206,9 @@ export const promise = async ({
|
||||
let tabPreviousSiblingDoc = siblingDoc
|
||||
|
||||
if (tabHasName(field)) {
|
||||
tabSiblingData = siblingData[field.name] as Record<string, unknown>
|
||||
tabSiblingDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
tabPreviousSiblingDoc = previousDoc[field.name] as Record<string, unknown>
|
||||
tabSiblingData = siblingData[field.name] as JsonObject
|
||||
tabSiblingDoc = siblingDoc[field.name] as JsonObject
|
||||
tabPreviousSiblingDoc = previousDoc[field.name] as JsonObject
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
|
||||
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { promise } from './promise.js'
|
||||
@@ -8,18 +8,18 @@ import { promise } from './promise.js'
|
||||
type Args = {
|
||||
collection: SanitizedCollectionConfig | null
|
||||
context: RequestContext
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
data: JsonObject
|
||||
doc: JsonObject
|
||||
fields: (Field | TabAsField)[]
|
||||
global: SanitizedGlobalConfig | null
|
||||
operation: 'create' | 'update'
|
||||
path: (number | string)[]
|
||||
previousDoc: Record<string, unknown>
|
||||
previousSiblingDoc: Record<string, unknown>
|
||||
previousDoc: JsonObject
|
||||
previousSiblingDoc: JsonObject
|
||||
req: PayloadRequest
|
||||
schemaPath: string[]
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
siblingData: JsonObject
|
||||
siblingDoc: JsonObject
|
||||
}
|
||||
|
||||
export const traverseFields = async ({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user