Compare commits

..

2 Commits

Author SHA1 Message Date
Jarrod Flesch
fa60901385 Merge branch 'beta' into fix/beta/field-path-props 2024-11-14 12:57:20 -05:00
Jarrod Flesch
9fc47cea6e chore: ensures all fields recieve parentPath and parentSchemaPath 2024-11-14 12:52:52 -05:00
257 changed files with 9406 additions and 10719 deletions

View File

@@ -39,7 +39,6 @@ body:
- 'db-postgres'
- 'db-sqlite'
- 'db-vercel-postgres'
- 'email-nodemailer'
- 'plugin: cloud'
- 'plugin: cloud-storage'
- 'plugin: form-builder'

View File

@@ -21,11 +21,10 @@ To do so, import the `useField` hook as follows:
```tsx
'use client'
import type { TextFieldClientComponent } from 'payload'
import { useField } from '@payloadcms/ui'
export const CustomTextField: TextFieldClientComponent = ({ path }) => {
const { value, setValue } = useField({ path }) // highlight-line
const CustomTextField: React.FC = () => {
const { value, setValue, path } = useField() // highlight-line
return (
<div>

View File

@@ -8,7 +8,7 @@ keywords: overview, config, configuration, documentation, Content Management Sys
Payload is a _config-based_, code-first CMS and application framework. The Payload Config is central to everything that Payload does, allowing for deep configuration of your application through a simple and intuitive API. The Payload Config is a fully-typed JavaScript object that can be infinitely extended upon.
Everything from your [Database](../database/overview) choice to the appearance of the [Admin Panel](../admin/overview) is fully controlled through the Payload Config. From here you can define [Fields](../fields/overview), add [Localization](./localization), enable [Authentication](../authentication/overview), configure [Access Control](../access-control/overview), and so much more.
Everything from your [Database](../database/overview) choice, to the appearance of the [Admin Panel](../admin/overview), is fully controlled through the Payload Config. From here you can define [Fields](../fields/overview), add [Localization](./localization), enable [Authentication](../authentication/overview), configure [Access Control](../access-control/overview), and so much more.
The Payload Config is a `payload.config.ts` file typically located in the root of your project:
@@ -29,7 +29,7 @@ The Payload Config is strongly typed and ties directly into Payload's TypeScript
## Config Options
To author your Payload Config, first determine which [Database](../database/overview) you'd like to use, then use [Collections](./collections) or [Globals](./globals) to define the schema of your data through [Fields](../fields/overview).
To author your Payload Config, first determine which [Database](../database/overview) you'd like to use, then use [Collections](./collections) or [Globals](./globals) to define the schema of your data.
Here is one of the simplest possible Payload configs:

View File

@@ -86,7 +86,7 @@ _\* This property is passed directly to [react-datepicker](https://github.com/Ha
These properties only affect how the date is displayed in the UI. The full date is always stored in the format `YYYY-MM-DDTHH:mm:ss.SSSZ` (e.g. `1999-01-01T8:00:00.000+05:00`).
`displayFormat` determines how the date is presented in the field **cell**, you can pass any valid (unicode date format)[https://date-fns.org/v4.1.0/docs/format].
`displayFormat` determines how the date is presented in the field **cell**, you can pass any valid (unicode date format)[https://date-fns.org/v2.29.3/docs/format].
`pickerAppearance` sets the appearance of the **react datepicker**, the options available are `dayAndTime`, `dayOnly`, `timeOnly`, and `monthOnly`. By default, the datepicker will display `dayOnly`.

View File

@@ -467,6 +467,20 @@ Payload is still headless, you will still be able to leverage it completely head
}
```
1. `root` endpoints no longer exist on the config. Instead, you must add them to the `/api` folder within your own Payload application.
```diff
// payload.config.ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
- endpoints: [...]
})
```
For more details, see the [Next.js Documentation](https://nextjs.org/docs/app/api-reference/file-conventions/route).
1. All other endpoint handlers have changed. The args no longer include `res`, and `next`, and the return type now expects a valid HTTP [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) instead of `res.json`, `res.send`, etc.:
```diff
@@ -684,7 +698,7 @@ import { s3Adapter } from '@payloadcms/plugin-cloud-storage/s3'
plugins: [
cloudStorage({
collections: {
media: {
[mediaSlug]: {
adapter: s3Adapter({
bucket: process.env.S3_BUCKET,
config: {
@@ -707,7 +721,7 @@ plugins: [
plugins: [
s3Storage({
collections: {
media: true,
[mediaSlug]: true,
},
bucket: process.env.S3_BUCKET,
config: {

View File

@@ -52,8 +52,8 @@ The following operators are available for use in queries:
| `all` | The value must contain all values provided in the comma-delimited list. |
| `exists` | Only return documents where the value either exists (`true`) or does not exist (`false`). |
| `near` | For distance related to a [Point Field](../fields/point) comma separated as `<longitude>, <latitude>, <maxDistance in meters (nullable)>, <minDistance in meters (nullable)>`. |
| `within` | For [Point Fields](../fields/point) to filter documents based on whether points are inside of the given area defined in GeoJSON. [Example](../fields/point#querying-within) |
| `intersects` | For [Point Fields](../fields/point) to filter documents based on whether points intersect with the given area defined in GeoJSON. [Example](../fields/point#querying-intersects) |
| `within` | For [point fields][../fields/point] to filter documents based on whether points are inside of the given area defined in GeoJSON. [Example](../fields/point#querying---within) |
| `intersects` | For [point fields][../fields/point] to filter documents based on whether points intersect with the given area defined in GeoJSON. [Example](../fields/point#querying---intersects) |
<Banner type="success">
<strong>Tip:</strong>

View File

@@ -14,7 +14,7 @@ Payload offers additional storage adapters to handle file uploads. These adapter
| AWS S3 | [`@payloadcms/storage-s3`](https://github.com/payloadcms/payload/tree/beta/packages/storage-s3) |
| Azure | [`@payloadcms/storage-azure`](https://github.com/payloadcms/payload/tree/beta/packages/storage-azure) |
| Google Cloud Storage | [`@payloadcms/storage-gcs`](https://github.com/payloadcms/payload/tree/beta/packages/storage-gcs) |
| Uploadthing | [`@payloadcms/storage-uploadthing`](https://github.com/payloadcms/payload/tree/beta/packages/uploadthing) |
## Vercel Blob Storage
[`@payloadcms/storage-vercel-blob`](https://www.npmjs.com/package/@payloadcms/storage-vercel-blob)
@@ -43,8 +43,8 @@ export default buildConfig({
enabled: true, // Optional, defaults to true
// Specify which collections should use Vercel Blob
collections: {
media: true,
'media-with-prefix': {
[Media.slug]: true,
[MediaWithPrefix.slug]: {
prefix: 'my-prefix',
},
},
@@ -90,8 +90,8 @@ export default buildConfig({
plugins: [
s3Storage({
collections: {
media: true,
'media-with-prefix': {
[mediaSlug]: true,
[mediaWithPrefixSlug]: {
prefix,
},
},
@@ -137,8 +137,8 @@ export default buildConfig({
plugins: [
azureStorage({
collections: {
media: true,
'media-with-prefix': {
[mediaSlug]: true,
[mediaWithPrefixSlug]: {
prefix,
},
},
@@ -186,8 +186,8 @@ export default buildConfig({
plugins: [
gcsStorage({
collections: {
media: true,
'media-with-prefix': {
[mediaSlug]: true,
[mediaWithPrefixSlug]: {
prefix,
},
},
@@ -224,7 +224,7 @@ pnpm add @payloadcms/storage-uploadthing@beta
### Usage
- Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type.
- Get a token from Uploadthing and set it as `token` in the `options` object.
- Get an API key from Uploadthing and set it as `apiKey` in the `options` object.
- `acl` is optional and defaults to `public-read`.
```ts
@@ -233,10 +233,10 @@ export default buildConfig({
plugins: [
uploadthingStorage({
collections: {
media: true,
[mediaSlug]: true,
},
options: {
token: process.env.UPLOADTHING_TOKEN,
apiKey: process.env.UPLOADTHING_SECRET,
acl: 'public-read',
},
}),
@@ -248,7 +248,7 @@ export default buildConfig({
| Option | Description | Default |
| ---------------- | ----------------------------------------------- | ------------- |
| `token` | Token from Uploadthing. Required. | |
| `apiKey` | API key from Uploadthing. Required. | |
| `acl` | Access control list for files that are uploaded | `public-read` |
| `logLevel` | Log level for Uploadthing | `info` |
| `fetch` | Custom fetch function | `fetch` |

View File

@@ -1,5 +1,4 @@
DATABASE_URI=mongodb://127.0.0.1/payload-example-email
MONGODB_URI=mongodb://127.0.0.1/payload-example-email
PAYLOAD_SECRET=
NODE_ENV=development
PAYLOAD_SECRET=PAYLOAD_EMAIL_EXAMPLE_SECRET_KEY
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:8000

View File

@@ -1,4 +1,7 @@
module.exports = {
root: true,
extends: ['@payloadcms'],
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
},
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
},
"transform": {
"react": {
"runtime": "automatic",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"development": false,
"useBuiltins": true
}
}
},
"module": {
"type": "es6"
}
}

View File

@@ -7,31 +7,30 @@ This example demonstrates how to integrate email functionality into Payload.
To spin up this example locally, follow these steps:
1. Clone this repo
2. `cp .env.example .env` to copy the example environment variables
3. `pnpm install && pnpm dev` to install dependencies and start the dev server
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. open `http://localhost:3000/admin` to access the admin panel
5. `open http://localhost:8000/admin` to access the admin panel
6. Create your first user
## How it works
Email functionality in Payload is configured using adapters. The recommended adapter for most use cases is the [@payloadcms/email-nodemailer](https://www.npmjs.com/package/@payloadcms/email-nodemailer) package.
Payload utilizes [NodeMailer](https://nodemailer.com/about/) for email functionality. Once you add your email configuration to `payload.init()`, you send email from anywhere in your application just by calling `payload.sendEmail({})`.
To enable email, pass your adapter configuration to the `email` property in the Payload Config. This allows Payload to send auth-related emails for password resets, new user verifications, and other email needs.
1. In the Payload Config file, add your email adapter to the `email` property. For example, the `@payloadcms/email-nodemailer` adapter can be configured for SMTP, SendGrid, or other supported transports. During development, if no configuration is provided, Payload will use a mock service via [ethereal.email](ethereal.email).
1. Navigate to `src/server.ts` - this is where your email config gets passed to Payload
2. Open `src/email/transport.ts` - here we are defining the email config. You can use an env variable to switch between the mock email transport and live email service.
Now we can start sending email!
2. Go to `src/collections/Newsletter.ts` - with an `afterChange` hook, we are sending an email when a new user signs up for the newsletter
3. Go to `src/collections/Newsletter.ts` - with an `afterChange` hook, we are sending an email when a new user signs up for the newsletter
Let's not forget our authentication emails...
3. Auth-enabled collections have built-in options to verify the user and reset the user password. Open `src/collections/Users.ts` and see how we customize these emails.
4. Auth-enabled collections have built-in options to verify the user and reset the user password. Open `src/collections/Users.ts` and see how we customize these emails.
Speaking of customization...
4. Take a look at `src/email/generateEmailHTML` and how it compiles a custom template when sending email. You change this to any HTML template of your choosing.
5. Take a look at `src/email/generateEmailHTML` and how it compiles a custom template when sending email. You change this to any HTML template of your choosing.
That's all you need, now you can go ahead and test out this repo by creating a new `user` or `newsletter-signup` and see the email integration in action.
@@ -41,10 +40,10 @@ To spin up this example locally, follow the [Quick Start](#quick-start).
## Production
To run Payload in production, you need to build and start the Admin panel. To do so, follow these steps:
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
1. Invoke the `next build` script by running `pnpm build` or `npm run build` in your project root. This creates a `.next` directory with a production-ready admin bundle.
1. Finally run `pnpm start` or `npm run start` to run Node in production and serve Payload from the `.build` directory.
1. First invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
1. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
### Deployment

View File

@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View File

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

View File

@@ -0,0 +1,5 @@
{
"ext": "ts",
"exec": "ts-node src/server.ts -- -I",
"stdin": false
}

View File

@@ -1,57 +1,35 @@
{
"name": "payload-example-email",
"version": "1.0.0",
"description": "Payload Email integration example.",
"version": "1.0.0",
"main": "dist/server.js",
"license": "MIT",
"type": "module",
"scripts": {
"_dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"dev": "cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true NODE_OPTIONS=--no-deprecation next dev",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:schema": "payload-graphql generate:schema",
"generate:types": "payload generate:types",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
"dev": "cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
"lint": "eslint src",
"lint:fix": "eslint --fix --ext .ts,.tsx src"
},
"dependencies": {
"@payloadcms/db-mongodb": "beta",
"@payloadcms/email-nodemailer": "beta",
"@payloadcms/next": "beta",
"@payloadcms/richtext-lexical": "beta",
"@payloadcms/ui": "beta",
"cross-env": "^7.0.3",
"dotenv": "^8.2.0",
"ejs": "3.1.10",
"graphql": "^16.9.0",
"juice": "11.0.0",
"next": "15.0.0",
"payload": "beta",
"react": "19.0.0-rc-65a56d0e-20241020",
"react-dom": "19.0.0-rc-65a56d0e-20241020"
"express": "^4.17.1",
"payload": "latest",
"handlebars": "^4.7.7",
"inline-css": "^4.0.2"
},
"devDependencies": {
"@payloadcms/graphql": "beta",
"@swc/core": "^1.6.13",
"@types/ejs": "^3.1.5",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"eslint": "^8.57.0",
"eslint-config-next": "15.0.0",
"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-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
}
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
"@types/express": "^4.17.9",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"eslint": "^8.19.0",
"nodemon": "^2.0.6",
"ts-node": "^9.1.1",
"typescript": "^4.8.4"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
import { importMap } from '../importMap'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, importMap, params, searchParams })
export default NotFound

View File

@@ -1,25 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
import { importMap } from '../importMap'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, importMap, params, searchParams })
export default Page

View File

@@ -1 +0,0 @@
export const importMap = {}

View File

@@ -1,10 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View File

@@ -1,8 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,32 +0,0 @@
import type { ServerFunctionClient } from 'payload'
import '@payloadcms/next/css'
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode
}
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)
export default Layout

View File

@@ -1,12 +1,28 @@
import type { CollectionConfig } from 'payload'
import type { CollectionConfig } from 'payload/types'
import generateEmailHTML from '../email/generateEmailHTML'
import { generateEmailHTML } from '../email/generateEmailHTML'
export const Newsletter: CollectionConfig = {
const Newsletter: CollectionConfig = {
slug: 'newsletter-signups',
admin: {
defaultColumns: ['name', 'email'],
},
hooks: {
afterChange: [
async ({ doc, operation, req }) => {
if (operation === 'create') {
req.payload.sendEmail({
to: doc.email,
from: 'sender@example.com',
subject: 'Thanks for signing up!',
html: await generateEmailHTML({
headline: 'Welcome to the newsletter!',
content: `<p>${doc.name ? `Hi ${doc.name}!` : 'Hi!'} We'll be in touch soon...</p>`,
}),
})
}
},
],
},
fields: [
{
name: 'name',
@@ -18,25 +34,6 @@ export const Newsletter: CollectionConfig = {
required: true,
},
],
hooks: {
afterChange: [
async ({ doc, operation, req }) => {
if (operation === 'create') {
req.payload
.sendEmail({
from: 'sender@example.com',
html: await generateEmailHTML({
content: `<p>${doc.name ? `Hi ${doc.name}!` : 'Hi!'} We'll be in touch soon...</p>`,
headline: 'Welcome to the newsletter!',
}),
subject: 'Thanks for signing up!',
to: doc.email,
})
.catch((error) => {
console.error('Error sending email:', error)
})
}
},
],
},
}
export default Newsletter

View File

@@ -1,23 +1,23 @@
import type { CollectionConfig } from 'payload'
import type { CollectionConfig } from 'payload/types'
import { generateForgotPasswordEmail } from '../email/generateForgotPasswordEmail'
import { generateVerificationEmail } from '../email/generateVerificationEmail'
import generateForgotPasswordEmail from '../email/generateForgotPasswordEmail'
import generateVerificationEmail from '../email/generateVerificationEmail'
export const Users: CollectionConfig = {
const Users: CollectionConfig = {
slug: 'users',
auth: {
verify: {
generateEmailSubject: () => 'Verify your email',
generateEmailHTML: generateVerificationEmail,
},
forgotPassword: {
generateEmailSubject: () => 'Reset your password',
generateEmailHTML: generateForgotPasswordEmail,
},
},
admin: {
useAsTitle: 'email',
},
auth: {
forgotPassword: {
generateEmailHTML: generateForgotPasswordEmail,
generateEmailSubject: () => 'Reset your password',
},
verify: {
generateEmailHTML: generateVerificationEmail,
generateEmailSubject: () => 'Verify your email',
},
},
fields: [
{
name: 'name',
@@ -25,3 +25,5 @@ export const Users: CollectionConfig = {
},
],
}
export default Users

View File

@@ -1,17 +1,24 @@
import ejs from 'ejs'
import fs from 'fs'
import juice from 'juice'
import Handlebars from 'handlebars'
import inlineCSS from 'inline-css'
import path from 'path'
export const generateEmailHTML = async (data: any): Promise<string> => {
const templatePath = path.join(process.cwd(), 'src/email/template.ejs')
const templateContent = fs.readFileSync(templatePath, 'utf8')
const template = fs.readFileSync
? fs.readFileSync(path.join(__dirname, './template.html'), 'utf8')
: ''
// Compile and render the template with EJS
const preInlinedCSS = ejs.render(templateContent, { ...data, cta: data.cta || {} })
// Compile the template
const getHTML = Handlebars.compile(template)
// Inline CSS
const html = juice(preInlinedCSS)
const generateEmailHTML = async (data): Promise<string> => {
const preInlinedCSS = getHTML(data)
return Promise.resolve(html)
const html = await inlineCSS(preInlinedCSS, {
url: ' ',
removeStyleTags: false,
})
return html
}
export default generateEmailHTML

View File

@@ -1,24 +1,13 @@
import type { PayloadRequest } from 'payload'
import generateEmailHTML from './generateEmailHTML'
import { generateEmailHTML } from './generateEmailHTML'
type ForgotPasswordEmailArgs =
| {
req?: PayloadRequest
token?: string
user?: any
}
| undefined
export const generateForgotPasswordEmail = async (
args: ForgotPasswordEmailArgs,
): Promise<string> => {
return generateEmailHTML({
const generateForgotPasswordEmail = async ({ token }): Promise<string> =>
generateEmailHTML({
headline: 'Locked out?',
content: '<p>Let&apos;s get you back in.</p>',
cta: {
buttonLabel: 'Reset your password',
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/reset-password?token=${args?.token}`,
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/reset-password?token=${token}`,
},
headline: 'Locked out?',
})
}
export default generateForgotPasswordEmail

View File

@@ -1,26 +1,16 @@
import { generateEmailHTML } from './generateEmailHTML'
import generateEmailHTML from './generateEmailHTML'
type User = {
email: string
name?: string
}
type GenerateVerificationEmailArgs = {
token: string
user: User
}
export const generateVerificationEmail = async (
args: GenerateVerificationEmailArgs,
): Promise<string> => {
const { token, user } = args
const generateVerificationEmail = async (args): Promise<string> => {
const { user, token } = args
return generateEmailHTML({
headline: 'Verify your account',
content: `<p>Hi${user.name ? ' ' + user.name : ''}! Validate your account by clicking the button below.</p>`,
cta: {
buttonLabel: 'Verify',
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/verify?token=${token}&email=${user.email}`,
},
headline: 'Verify your account',
})
}
export default generateVerificationEmail

View File

@@ -1,327 +0,0 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style type="text/css">
body,
html {
margin: 0;
padding: 0;
}
body,
html,
.bg {
height: 100%;
}
body,
h1,
h2,
h3,
h4,
p,
em,
strong {
font-family: sans-serif;
}
body {
font-size: 15px;
color: #333333;
}
a {
color: #333333;
outline: 0;
text-decoration: underline;
}
a img {
border: 0;
outline: 0;
}
img {
max-width: 100%;
height: auto;
vertical-align: top;
}
h1,
h2,
h3,
h4,
h5 {
font-weight: 900;
line-height: 1.25;
}
h1 {
font-size: 40px;
color: #333333;
margin: 0 0 25px 0;
}
h2 {
color: #333333;
margin: 0 0 25px 0;
font-size: 30px;
line-height: 30px;
}
h3 {
font-size: 25px;
color: #333333;
margin: 0 0 25px 0;
}
h4 {
font-size: 20px;
color: #333333;
margin: 0 0 15px 0;
line-height: 30px;
}
h5 {
color: #333333;
font-size: 17px;
font-weight: 900;
margin: 0 0 15px;
}
table {
border-collapse: collapse;
}
p,
td {
font-size: 14px;
line-height: 25px;
color: #333333;
}
p {
margin: 0 0 25px;
}
ul {
padding-left: 15px;
margin-left: 15px;
font-size: 14px;
line-height: 25px;
margin-bottom: 25px;
}
li {
font-size: 14px;
line-height: 25px;
color: #333333;
}
table.hr td {
font-size: 0;
line-height: 2px;
}
.white {
color: white;
}
/********************************
MAIN
********************************/
.main {
background: white;
}
/********************************
MAX WIDTHS
********************************/
.max-width {
max-width: 800px;
width: 94%;
margin: 0 3%;
}
/********************************
REUSABLES
********************************/
.padding {
padding: 60px;
}
.center {
text-align: center;
}
.no-border {
border: 0;
outline: none;
text-decoration: none;
}
.no-margin {
margin: 0;
}
.spacer {
line-height: 45px;
height: 45px;
}
/********************************
PANELS
********************************/
.panel {
width: 100%;
}
@media screen and (max-width: 800px) {
h1 {
font-size: 24px !important;
margin: 0 0 20px 0 !important;
}
h2 {
font-size: 20px !important;
margin: 0 0 20px 0 !important;
}
h3 {
font-size: 20px !important;
margin: 0 0 20px 0 !important;
}
h4 {
font-size: 18px !important;
margin: 0 0 15px 0 !important;
}
h5 {
font-size: 15px !important;
margin: 0 0 10px !important;
}
.max-width {
width: 90% !important;
margin: 0 5% !important;
}
td.padding {
padding: 30px !important;
}
td.padding-vert {
padding-top: 20px !important;
padding-bottom: 20px !important;
}
td.padding-horiz {
padding-left: 20px !important;
padding-right: 20px !important;
}
.spacer {
line-height: 20px !important;
height: 20px !important;
}
}
</style>
</head>
<body>
<div style="background-color: #f3f3f3; height: 100%">
<table height="100%" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f3f3f3">
<tr>
<td valign="top" align="left">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tbody>
<tr>
<td align="center" valign="top">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tbody>
<tr>
<td align="center">
<table class="max-width" cellpadding="0" cellspacing="0" border="0" width="100%">
<tbody>
<tr>
<td class="spacer">&nbsp;</td>
</tr>
<tr>
<td class="padding main">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tbody>
<tr>
<td>
<!-- LOGO -->
<a href="https://payloadcms.com/" target="_blank">
<img src="https://payloadcms.com/images/logo-dark.png" width="150"
height="auto" />
</a>
</td>
</tr>
<tr>
<td class="spacer">&nbsp;</td>
</tr>
<tr>
<td>
<!-- HEADLINE -->
<h1 style="margin: 0 0 30px"><%= headline %></h1>
</td>
</tr>
<tr>
<td>
<!-- CONTENT -->
<%- content %>
<!-- CTA -->
<% if (cta) { %>
<div>
<a href="<%= cta.url %>" style="
background-color: #222222;
border-radius: 4px;
color: #ffffff;
display: inline-block;
font-family: sans-serif;
font-size: 13px;
font-weight: bold;
line-height: 60px;
text-align: center;
text-decoration: none;
width: 200px;
-webkit-text-size-adjust: none;
">
<%= cta.buttonLabel %>
</a>
</div>
<% } %>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>

View File

@@ -0,0 +1,345 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style type="text/css">
body,
html {
margin: 0;
padding: 0;
}
body,
html,
.bg {
height: 100%;
}
body,
h1,
h2,
h3,
h4,
p,
em,
strong {
font-family: sans-serif;
}
body {
font-size: 15px;
color: #333333;
}
a {
color: #333333;
outline: 0;
text-decoration: underline;
}
a img {
border: 0;
outline: 0;
}
img {
max-width: 100%;
height: auto;
vertical-align: top;
}
h1,
h2,
h3,
h4,
h5 {
font-weight: 900;
line-height: 1.25;
}
h1 {
font-size: 40px;
color: #333333;
margin: 0 0 25px 0;
}
h2 {
color: #333333;
margin: 0 0 25px 0;
font-size: 30px;
line-height: 30px;
}
h3 {
font-size: 25px;
color: #333333;
margin: 0 0 25px 0;
}
h4 {
font-size: 20px;
color: #333333;
margin: 0 0 15px 0;
line-height: 30px;
}
h5 {
color: #333333;
font-size: 17px;
font-weight: 900;
margin: 0 0 15px;
}
table {
border-collapse: collapse;
}
p,
td {
font-size: 14px;
line-height: 25px;
color: #333333;
}
p {
margin: 0 0 25px;
}
ul {
padding-left: 15px;
margin-left: 15px;
font-size: 14px;
line-height: 25px;
margin-bottom: 25px;
}
li {
font-size: 14px;
line-height: 25px;
color: #333333;
}
table.hr td {
font-size: 0;
line-height: 2px;
}
.white {
color: white;
}
/********************************
MAIN
********************************/
.main {
background: white;
}
/********************************
MAX WIDTHS
********************************/
.max-width {
max-width: 800px;
width: 94%;
margin: 0 3%;
}
/********************************
REUSABLES
********************************/
.padding {
padding: 60px;
}
.center {
text-align: center;
}
.no-border {
border: 0;
outline: none;
text-decoration: none;
}
.no-margin {
margin: 0;
}
.spacer {
line-height: 45px;
height: 45px;
}
/********************************
PANELS
********************************/
.panel {
width: 100%;
}
@media screen and (max-width: 800px) {
h1 {
font-size: 24px !important;
margin: 0 0 20px 0 !important;
}
h2 {
font-size: 20px !important;
margin: 0 0 20px 0 !important;
}
h3 {
font-size: 20px !important;
margin: 0 0 20px 0 !important;
}
h4 {
font-size: 18px !important;
margin: 0 0 15px 0 !important;
}
h5 {
font-size: 15px !important;
margin: 0 0 10px !important;
}
.max-width {
width: 90% !important;
margin: 0 5% !important;
}
td.padding {
padding: 30px !important;
}
td.padding-vert {
padding-top: 20px !important;
padding-bottom: 20px !important;
}
td.padding-horiz {
padding-left: 20px !important;
padding-right: 20px !important;
}
.spacer {
line-height: 20px !important;
height: 20px !important;
}
}
</style>
</head>
<body>
<div style="background-color: #f3f3f3; height: 100%">
<table
height="100%"
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
bgcolor="#f3f3f3"
style="background-color: #f3f3f3"
>
<tr>
<td valign="top" align="left">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tbody>
<tr>
<td align="center" valign="top">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tbody>
<tr>
<td align="center">
<table
class="max-width"
cellpadding="0"
cellspacing="0"
border="0"
width="100%"
style="width: 100%"
>
<tbody>
<tr>
<td class="spacer">&nbsp;</td>
</tr>
<tr>
<td class="padding main">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tbody>
<tr>
<td>
<!-- LOGO -->
<a href="https://payloadcms.com/" target="_blank">
<img
src="https://payloadcms.com/images/logo-dark.png"
width="150"
height="auto"
/>
</a>
</td>
</tr>
<tr>
<td class="spacer">&nbsp;</td>
</tr>
<tr>
<td>
<!-- HEADLINE -->
<h1 style="margin: 0 0 30px">{{headline}}</h1>
</td>
</tr>
<tr>
<td>
<!-- CONTENT -->
{{{content}}}
<!-- CTA -->
{{#if cta}}
<div>
<a
href="{{cta.url}}"
style="
background-color: #222222;
border-radius: 4px;
color: #ffffff;
display: inline-block;
font-family: sans-serif;
font-size: 13px;
font-weight: bold;
line-height: 60px;
text-align: center;
text-decoration: none;
width: 200px;
-webkit-text-size-adjust: none;
"
>
{{cta.buttonLabel}}
</a>
</div>
{{/if}}
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>

View File

@@ -0,0 +1,19 @@
let email
if (process.env.NODE_ENV === 'production') {
email = {
fromName: 'Payload',
fromAddress: 'info@payloadcms.com',
transportOptions: {
// Configure a custom transport here
},
}
} else {
email = {
fromName: 'Ethereal Email',
fromAddress: 'example@ethereal.com',
logMockCredentials: true,
}
}
export default email

View File

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

View File

@@ -1,5 +1,4 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
@@ -7,213 +6,30 @@
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
'newsletter-signups': NewsletterSignup;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
'newsletter-signups': NewsletterSignupsSelect<false> | NewsletterSignupsSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {};
globalsSelect: {};
locale: null;
user: User & {
collection: 'users';
};
jobs?: {
tasks: unknown;
workflows?: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "newsletter-signups".
*/
export interface NewsletterSignup {
id: string;
name?: string | null;
name?: string;
email: string;
updatedAt: string;
createdAt: string;
updatedAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string;
name?: string | null;
updatedAt: string;
name?: string;
email?: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
_verified?: boolean;
_verificationToken?: string;
loginAttempts?: number;
lockUntil?: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
_verified?: boolean | null;
_verificationToken?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?:
| ({
relationTo: 'newsletter-signups';
value: string | NewsletterSignup;
} | null)
| ({
relationTo: 'users';
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
password?: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "newsletter-signups_select".
*/
export interface NewsletterSignupsSelect<T extends boolean = true> {
name?: T;
email?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
name?: T;
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
_verified?: T;
_verificationToken?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {
export interface GeneratedTypes extends Config {}
}

View File

@@ -1,37 +1,42 @@
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import dotenv from 'dotenv'
import path from 'path'
import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'
import { buildConfig } from 'payload/config'
import { Newsletter } from './collections/Newsletter'
import { Users } from './collections/Users'
import Users from './collections/Users'
import Newsletter from './collections/Newsletter'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
dotenv.config({
path: path.resolve(__dirname, '../.env'),
})
const mockModulePath = path.resolve(__dirname, './emptyModule.js')
// eslint-disable-next-line no-restricted-exports
export default buildConfig({
admin: {
importMap: {
baseDir: path.resolve(dirname),
},
user: Users.slug,
webpack: (config) => ({
...config,
resolve: {
...config?.resolve,
alias: [
'fs',
'handlebars',
'inline-css',
path.resolve(__dirname, './email/transport'),
path.resolve(__dirname, './email/generateEmailHTML'),
path.resolve(__dirname, './email/generateForgotPasswordEmail'),
path.resolve(__dirname, './email/generateVerificationEmail'),
].reduce(
(aliases, importPath) => ({
...aliases,
[importPath]: mockModulePath,
}),
config.resolve.alias,
),
},
}),
},
collections: [Newsletter, Users],
db: mongooseAdapter({
url: process.env.DATABASE_URI || '',
}),
editor: lexicalEditor({}),
// For example use case, we are passing nothing to nodemailerAdapter
// This will default to using etherial.email
email: nodemailerAdapter(),
graphQL: {
schemaOutputFile: path.resolve(dirname, 'generated-schema.graphql'),
},
secret: process.env.PAYLOAD_SECRET || '',
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
outputFile: path.resolve(__dirname, 'payload-types.ts'),
},
})

View File

@@ -0,0 +1,30 @@
import express from 'express'
import path from 'path'
import payload from 'payload'
import email from './email/transport'
require('dotenv').config({
path: path.resolve(__dirname, '../.env'),
})
const app = express()
app.get('/', (_, res) => {
res.redirect('/admin')
})
const start = async (): Promise<void> => {
await payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
express: app,
email,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
},
})
app.listen(8000)
}
start()

View File

@@ -1,48 +1,24 @@
{
"compilerOptions": {
"baseUrl": ".",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"strict": false,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
],
"@payload-config": [
"src/payload.config.ts"
],
"@payload-types": [
"src/payload-types.ts"
]
},
"target": "ES2017"
"payload/generated-types": ["./src/payload-types.ts"],
"node_modules/*": ["./node_modules/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"src/mocks/emptyObject.js"
],
"exclude": [
"node_modules"
]
"include": ["src"],
"exclude": ["node_modules", "dist", "build"],
"ts-node": {
"transpileOnly": true
}
}

7942
examples/email/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -71,7 +71,7 @@ const vercelBlobStorageReplacement: StorageAdapterReplacement = {
configReplacement: [
' vercelBlobStorage({',
' collections: {',
' media: true,',
' [Media.slug]: true,',
' },',
" token: process.env.BLOB_READ_WRITE_TOKEN || '',",
' }),',

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -23,11 +23,6 @@
"import": "./src/index.ts",
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./migration-utils": {
"import": "./src/exports/migration-utils.ts",
"types": "./src/exports/migration-utils.ts",
"default": "./src/exports/migration-utils.ts"
}
},
"main": "./src/index.ts",
@@ -46,17 +41,18 @@
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"bson-objectid": "2.0.4",
"http-status": "1.6.2",
"mongoose": "8.8.1",
"mongoose-aggregate-paginate-v2": "1.1.2",
"mongoose-paginate-v2": "1.8.5",
"mongoose": "6.12.3",
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2",
"uuid": "10.0.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/mongoose-aggregate-paginate-v2": "1.0.12",
"mongodb": "6.10.0",
"@types/mongoose-aggregate-paginate-v2": "1.0.6",
"mongodb": "4.17.1",
"mongodb-memory-server": "^9",
"payload": "workspace:*"
},
@@ -69,11 +65,6 @@
"import": "./dist/index.js",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./migration-utils": {
"import": "./dist/exports/migration-utils.js",
"types": "./dist/exports/migration-utils.d.ts",
"default": "./dist/exports/migration-utils.js"
}
},
"main": "./dist/index.js",

View File

@@ -60,7 +60,14 @@ export const connect: Connect = async function connect(
if (this.ensureIndexes) {
await Promise.all(
this.payload.config.collections.map(async (coll) => {
await this.collections[coll.slug]?.ensureIndexes()
await new Promise((resolve, reject) => {
this.collections[coll.slug]?.ensureIndexes(function (err) {
if (err) {
reject(err)
}
resolve(true)
})
})
}),
)
}

View File

@@ -1,4 +1,4 @@
import type { CountOptions } from 'mongodb'
import type { QueryOptions } from 'mongoose'
import type { Count, PayloadRequest } from 'payload'
import { flattenWhereToOperators } from 'payload'
@@ -12,7 +12,7 @@ export const count: Count = async function count(
{ collection, locale, req = {} as PayloadRequest, where },
) {
const Model = this.collections[collection]
const options: CountOptions = await withSession(this, req)
const options: QueryOptions = await withSession(this, req)
let hasNearConstraint = false
@@ -40,12 +40,7 @@ export const count: Count = async function count(
}
}
let result: number
if (useEstimatedCount) {
result = await Model.estimatedDocumentCount({ session: options.session })
} else {
result = await Model.countDocuments(query, options)
}
const result = await Model.countDocuments(query, options)
return {
totalDocs: result,

View File

@@ -1,4 +1,4 @@
import type { CountOptions } from 'mongodb'
import type { QueryOptions } from 'mongoose'
import type { CountGlobalVersions, PayloadRequest } from 'payload'
import { flattenWhereToOperators } from 'payload'
@@ -12,7 +12,7 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob
{ global, locale, req = {} as PayloadRequest, where },
) {
const Model = this.versions[global]
const options: CountOptions = await withSession(this, req)
const options: QueryOptions = await withSession(this, req)
let hasNearConstraint = false
@@ -40,12 +40,7 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob
}
}
let result: number
if (useEstimatedCount) {
result = await Model.estimatedDocumentCount({ session: options.session })
} else {
result = await Model.countDocuments(query, options)
}
const result = await Model.countDocuments(query, options)
return {
totalDocs: result,

View File

@@ -1,4 +1,4 @@
import type { CountOptions } from 'mongodb'
import type { QueryOptions } from 'mongoose'
import type { CountVersions, PayloadRequest } from 'payload'
import { flattenWhereToOperators } from 'payload'
@@ -12,7 +12,7 @@ export const countVersions: CountVersions = async function countVersions(
{ collection, locale, req = {} as PayloadRequest, where },
) {
const Model = this.versions[collection]
const options: CountOptions = await withSession(this, req)
const options: QueryOptions = await withSession(this, req)
let hasNearConstraint = false
@@ -40,12 +40,7 @@ export const countVersions: CountVersions = async function countVersions(
}
}
let result: number
if (useEstimatedCount) {
result = await Model.estimatedDocumentCount({ session: options.session })
} else {
result = await Model.countDocuments(query, options)
}
const result = await Model.countDocuments(query, options)
return {
totalDocs: result,

View File

@@ -1,4 +1,4 @@
import { Types } from 'mongoose'
import mongoose from 'mongoose'
import {
buildVersionCollectionFields,
type CreateVersion,
@@ -57,7 +57,7 @@ export const createVersion: CreateVersion = async function createVersion(
},
],
}
if (data.parent instanceof Types.ObjectId) {
if (data.parent instanceof mongoose.Types.ObjectId) {
parentQuery.$or.push({
parent: {
$eq: data.parent.toString(),

View File

@@ -1,2 +0,0 @@
export { migrateRelationshipsV2_V3 } from '../predefinedMigrations/migrateRelationshipsV2_V3.js'
export { migrateVersionsV1_V2 } from '../predefinedMigrations/migrateVersionsV1_V2.js'

View File

@@ -58,6 +58,7 @@ export const find: Find = async function find(
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
const paginationOptions: PaginateOptions = {
forceCountFn: hasNearConstraint,
lean: true,
leanWithId: true,
options,

View File

@@ -64,6 +64,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
const paginationOptions: PaginateOptions = {
forceCountFn: hasNearConstraint,
lean: true,
leanWithId: true,
limit,

View File

@@ -60,6 +60,7 @@ export const findVersions: FindVersions = async function findVersions(
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
const paginationOptions: PaginateOptions = {
forceCountFn: hasNearConstraint,
lean: true,
leanWithId: true,
limit,

View File

@@ -1,17 +1,7 @@
import type { CollationOptions, TransactionOptions } from 'mongodb'
import type { MongoMemoryReplSet } from 'mongodb-memory-server'
import type { ClientSession, Connection, ConnectOptions, QueryOptions } from 'mongoose'
import type {
BaseDatabaseAdapter,
DatabaseAdapterObj,
Payload,
TypeWithID,
TypeWithVersion,
UpdateGlobalArgs,
UpdateGlobalVersionArgs,
UpdateOneArgs,
UpdateVersionArgs,
} from 'payload'
import type { BaseDatabaseAdapter, DatabaseAdapterObj, Payload, UpdateOneArgs } from 'payload'
import fs from 'fs'
import mongoose from 'mongoose'
@@ -145,16 +135,7 @@ declare module 'payload' {
}[]
sessions: Record<number | string, ClientSession>
transactionOptions: TransactionOptions
updateGlobal: <T extends Record<string, unknown>>(
args: { options?: QueryOptions } & UpdateGlobalArgs<T>,
) => Promise<T>
updateGlobalVersion: <T extends TypeWithID = TypeWithID>(
args: { options?: QueryOptions } & UpdateGlobalVersionArgs<T>,
) => Promise<TypeWithVersion<T>>
updateOne: (args: { options?: QueryOptions } & UpdateOneArgs) => Promise<Document>
updateVersion: <T extends TypeWithID = TypeWithID>(
args: { options?: QueryOptions } & UpdateVersionArgs<T>,
) => Promise<TypeWithVersion<T>>
versions: {
[slug: string]: CollectionModel
}

View File

@@ -17,14 +17,14 @@ import { getDBName } from './utilities/getDBName.js'
export const init: Init = function init(this: MongooseAdapter) {
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const schema = buildCollectionSchema(collection, this.payload)
const schema = buildCollectionSchema(collection, this.payload.config)
if (collection.versions) {
const versionModelName = getDBName({ config: collection, versions: true })
const versionCollectionFields = buildVersionCollectionFields(this.payload.config, collection)
const versionSchema = buildSchema(this.payload, versionCollectionFields, {
const versionSchema = buildSchema(this.payload.config, versionCollectionFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
@@ -66,7 +66,7 @@ export const init: Init = function init(this: MongooseAdapter) {
) as CollectionModel
})
this.globals = buildGlobalModel(this.payload)
this.globals = buildGlobalModel(this.payload.config)
this.payload.config.globals.forEach((global) => {
if (global.versions) {
@@ -74,7 +74,7 @@ export const init: Init = function init(this: MongooseAdapter) {
const versionGlobalFields = buildVersionGlobalFields(this.payload.config, global)
const versionSchema = buildSchema(this.payload, versionGlobalFields, {
const versionSchema = buildSchema(this.payload.config, versionGlobalFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,

View File

@@ -1,5 +1,5 @@
import type { PaginateOptions, Schema } from 'mongoose'
import type { Payload, SanitizedCollectionConfig } from 'payload'
import type { SanitizedCollectionConfig, SanitizedConfig } from 'payload'
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
import paginate from 'mongoose-paginate-v2'
@@ -9,12 +9,12 @@ import { buildSchema } from './buildSchema.js'
export const buildCollectionSchema = (
collection: SanitizedCollectionConfig,
payload: Payload,
config: SanitizedConfig,
schemaOptions = {},
): Schema => {
const schema = buildSchema(payload, collection.fields, {
const schema = buildSchema(config, collection.fields, {
draftsEnabled: Boolean(typeof collection?.versions === 'object' && collection.versions.drafts),
indexSortableFields: payload.config.indexSortableFields,
indexSortableFields: config.indexSortableFields,
options: {
minimize: false,
timestamps: collection.timestamps !== false,
@@ -34,7 +34,7 @@ export const buildCollectionSchema = (
schema.index(indexDefinition, { unique: true })
}
if (payload.config.indexSortableFields && collection.timestamps !== false) {
if (config.indexSortableFields && collection.timestamps !== false) {
schema.index({ updatedAt: 1 })
schema.index({ createdAt: 1 })
}

View File

@@ -1,4 +1,4 @@
import type { Payload } from 'payload'
import type { SanitizedConfig } from 'payload'
import mongoose from 'mongoose'
@@ -7,8 +7,8 @@ import type { GlobalModel } from '../types.js'
import { getBuildQueryPlugin } from '../queries/buildQuery.js'
import { buildSchema } from './buildSchema.js'
export const buildGlobalModel = (payload: Payload): GlobalModel | null => {
if (payload.config.globals && payload.config.globals.length > 0) {
export const buildGlobalModel = (config: SanitizedConfig): GlobalModel | null => {
if (config.globals && config.globals.length > 0) {
const globalsSchema = new mongoose.Schema(
{},
{ discriminatorKey: 'globalType', minimize: false, timestamps: true },
@@ -18,8 +18,8 @@ export const buildGlobalModel = (payload: Payload): GlobalModel | null => {
const Globals = mongoose.model('globals', globalsSchema, 'globals') as unknown as GlobalModel
Object.values(payload.config.globals).forEach((globalConfig) => {
const globalSchema = buildSchema(payload, globalConfig.fields, {
Object.values(config.globals).forEach((globalConfig) => {
const globalSchema = buildSchema(config, globalConfig.fields, {
options: {
minimize: false,
},

View File

@@ -1,35 +1,35 @@
import type { IndexOptions, Schema, SchemaOptions, SchemaTypeOptions } from 'mongoose'
import type {
ArrayField,
Block,
BlocksField,
CheckboxField,
CodeField,
CollapsibleField,
DateField,
EmailField,
Field,
FieldAffectingData,
GroupField,
JSONField,
NonPresentationalField,
NumberField,
PointField,
RadioField,
RelationshipField,
RichTextField,
RowField,
SanitizedConfig,
SanitizedLocalizationConfig,
SelectField,
Tab,
TabsField,
TextareaField,
TextField,
UploadField,
} from 'payload'
import mongoose from 'mongoose'
import {
type ArrayField,
type Block,
type BlocksField,
type CheckboxField,
type CodeField,
type CollapsibleField,
type DateField,
type EmailField,
type Field,
type FieldAffectingData,
type GroupField,
type JSONField,
type NonPresentationalField,
type NumberField,
type Payload,
type PointField,
type RadioField,
type RelationshipField,
type RichTextField,
type RowField,
type SanitizedLocalizationConfig,
type SelectField,
type Tab,
type TabsField,
type TextareaField,
type TextField,
type UploadField,
} from 'payload'
import {
fieldAffectsData,
fieldIsLocalized,
@@ -49,7 +49,7 @@ export type BuildSchemaOptions = {
type FieldSchemaGenerator = (
field: Field,
schema: Schema,
config: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
) => void
@@ -113,7 +113,7 @@ const localizeSchema = (
}
export const buildSchema = (
payload: Payload,
config: SanitizedConfig,
configFields: Field[],
buildSchemaOptions: BuildSchemaOptions = {},
): Schema => {
@@ -145,7 +145,7 @@ export const buildSchema = (
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type]
if (addFieldSchema) {
addFieldSchema(field, schema, payload, buildSchemaOptions)
addFieldSchema(field, schema, config, buildSchemaOptions)
}
}
})
@@ -157,13 +157,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
array: (
field: ArrayField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
) => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
type: [
buildSchema(payload, field.fields, {
buildSchema(config, field.fields, {
allowIDField: true,
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
@@ -177,13 +177,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
blocks: (
field: BlocksField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const fieldSchema = {
@@ -191,7 +191,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, fieldSchema, payload.config.localization),
[field.name]: localizeSchema(field, fieldSchema, config.localization),
})
field.blocks.forEach((blockItem: Block) => {
@@ -200,12 +200,12 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
blockItem.fields.forEach((blockField) => {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]
if (addFieldSchema) {
addFieldSchema(blockField, blockSchema, payload, buildSchemaOptions)
addFieldSchema(blockField, blockSchema, config, buildSchemaOptions)
}
})
if (field.localized && payload.config.localization) {
payload.config.localization.localeCodes.forEach((localeCode) => {
if (field.localized && config.localization) {
config.localization.localeCodes.forEach((localeCode) => {
// @ts-expect-error Possible incorrect typing in mongoose types, this works
schema.path(`${field.name}.${localeCode}`).discriminator(blockItem.slug, blockSchema)
})
@@ -218,31 +218,31 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
checkbox: (
field: CheckboxField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean }
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
code: (
field: CodeField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
collapsible: (
field: CollapsibleField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
field.fields.forEach((subField: Field) => {
@@ -253,38 +253,38 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(subField, schema, payload, buildSchemaOptions)
addFieldSchema(subField, schema, config, buildSchemaOptions)
}
})
},
date: (
field: DateField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date }
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
email: (
field: EmailField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
group: (
field: GroupField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions)
@@ -297,7 +297,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const baseSchema = {
...formattedBaseSchema,
type: buildSchema(payload, field.fields, {
type: buildSchema(config, field.fields, {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
indexSortableFields,
@@ -310,13 +310,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
json: (
field: JSONField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
@@ -325,13 +325,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
number: (
field: NumberField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
@@ -340,13 +340,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
point: (
field: PointField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema: SchemaTypeOptions<unknown> = {
@@ -368,7 +368,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
if (field.index === true || field.index === undefined) {
@@ -377,8 +377,8 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
indexOptions.sparse = true
indexOptions.unique = true
}
if (field.localized && payload.config.localization) {
payload.config.localization.locales.forEach((locale) => {
if (field.localized && config.localization) {
config.localization.locales.forEach((locale) => {
schema.index({ [`${field.name}.${locale.code}`]: '2dsphere' }, indexOptions)
})
} else {
@@ -389,7 +389,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
radio: (
field: RadioField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
@@ -404,23 +404,21 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
relationship: (
field: RelationshipField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
) => {
const hasManyRelations = Array.isArray(field.relationTo)
let schemaToReturn: { [key: string]: any } = {}
const valueType = getRelationshipValueType(field, payload)
if (field.localized && payload.config.localization) {
if (field.localized && config.localization) {
schemaToReturn = {
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
type: config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {}
if (hasManyRelations) {
@@ -430,14 +428,14 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
value: {
type: valueType,
type: mongoose.Schema.Types.Mixed,
refPath: `${field.name}.${locale}.relationTo`,
},
}
} else {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
type: valueType,
type: mongoose.Schema.Types.Mixed,
ref: field.relationTo,
}
}
@@ -458,7 +456,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
value: {
type: valueType,
type: mongoose.Schema.Types.Mixed,
refPath: `${field.name}.relationTo`,
},
}
@@ -472,7 +470,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
} else {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
type: valueType,
type: mongoose.Schema.Types.Mixed,
ref: field.relationTo,
}
@@ -491,7 +489,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
richText: (
field: RichTextField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
@@ -500,13 +498,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
row: (
field: RowField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
field.fields.forEach((subField: Field) => {
@@ -517,14 +515,14 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(subField, schema, payload, buildSchemaOptions)
addFieldSchema(subField, schema, config, buildSchemaOptions)
}
})
},
select: (
field: SelectField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
@@ -546,14 +544,14 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
[field.name]: localizeSchema(
field,
field.hasMany ? [baseSchema] : baseSchema,
payload.config.localization,
config.localization,
),
})
},
tabs: (
field: TabsField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
field.tabs.forEach((tab) => {
@@ -562,7 +560,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
return
}
const baseSchema = {
type: buildSchema(payload, tab.fields, {
type: buildSchema(config, tab.fields, {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
options: {
@@ -574,7 +572,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[tab.name]: localizeSchema(tab, baseSchema, payload.config.localization),
[tab.name]: localizeSchema(tab, baseSchema, config.localization),
})
} else {
tab.fields.forEach((subField: Field) => {
@@ -584,7 +582,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(subField, schema, payload, buildSchemaOptions)
addFieldSchema(subField, schema, config, buildSchemaOptions)
}
})
}
@@ -593,7 +591,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
text: (
field: TextField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
@@ -602,35 +600,33 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
textarea: (
field: TextareaField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(field, baseSchema, config.localization),
})
},
upload: (
field: UploadField,
schema: Schema,
payload: Payload,
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const hasManyRelations = Array.isArray(field.relationTo)
let schemaToReturn: { [key: string]: any } = {}
const valueType = getRelationshipValueType(field, payload)
if (field.localized && payload.config.localization) {
if (field.localized && config.localization) {
schemaToReturn = {
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
type: config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {}
if (hasManyRelations) {
@@ -640,14 +636,14 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
value: {
type: valueType,
type: mongoose.Schema.Types.Mixed,
refPath: `${field.name}.${locale}.relationTo`,
},
}
} else {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
type: valueType,
type: mongoose.Schema.Types.Mixed,
ref: field.relationTo,
}
}
@@ -668,7 +664,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
value: {
type: valueType,
type: mongoose.Schema.Types.Mixed,
refPath: `${field.name}.relationTo`,
},
}
@@ -682,7 +678,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
} else {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
type: valueType,
type: mongoose.Schema.Types.Mixed,
ref: field.relationTo,
}
@@ -699,30 +695,3 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
})
},
}
const getRelationshipValueType = (field: RelationshipField | UploadField, payload: Payload) => {
if (typeof field.relationTo === 'string') {
const { customIDType } = payload.collections[field.relationTo]
if (!customIDType) {
return mongoose.Schema.Types.ObjectId
}
if (customIDType === 'number') {
return mongoose.Schema.Types.Number
}
return mongoose.Schema.Types.String
}
// has custom id relationTo
if (
field.relationTo.some((relationTo) => {
return !!payload.collections[relationTo].customIDType
})
) {
return mongoose.Schema.Types.Mixed
}
return mongoose.Schema.Types.ObjectId
}

View File

@@ -1,183 +0,0 @@
import type { ClientSession, Model } from 'mongoose'
import type { Field, PayloadRequest, SanitizedConfig } from 'payload'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import type { MongooseAdapter } from '../index.js'
import { sanitizeRelationshipIDs } from '../utilities/sanitizeRelationshipIDs.js'
import { withSession } from '../withSession.js'
const migrateModelWithBatching = async ({
batchSize,
config,
fields,
Model,
session,
}: {
batchSize: number
config: SanitizedConfig
fields: Field[]
Model: Model<any>
session: ClientSession
}): Promise<void> => {
let hasNext = true
let skip = 0
while (hasNext) {
const docs = await Model.find(
{},
{},
{
lean: true,
limit: batchSize + 1,
session,
skip,
},
)
if (docs.length === 0) {
break
}
hasNext = docs.length > batchSize
if (hasNext) {
docs.pop()
}
for (const doc of docs) {
sanitizeRelationshipIDs({ config, data: doc, fields })
}
await Model.collection.bulkWrite(
docs.map((doc) => ({
updateOne: {
filter: { _id: doc._id },
update: {
$set: doc,
},
},
})),
{ session },
)
skip += batchSize
}
}
const hasRelationshipOrUploadField = ({ fields }: { fields: Field[] }): boolean => {
for (const field of fields) {
if (field.type === 'relationship' || field.type === 'upload') {
return true
}
if ('fields' in field) {
if (hasRelationshipOrUploadField({ fields: field.fields })) {
return true
}
}
if ('blocks' in field) {
for (const block of field.blocks) {
if (hasRelationshipOrUploadField({ fields: block.fields })) {
return true
}
}
}
if ('tabs' in field) {
for (const tab of field.tabs) {
if (hasRelationshipOrUploadField({ fields: tab.fields })) {
return true
}
}
}
}
return false
}
export async function migrateRelationshipsV2_V3({
batchSize,
req,
}: {
batchSize: number
req: PayloadRequest
}): Promise<void> {
const { payload } = req
const db = payload.db as MongooseAdapter
const config = payload.config
const { session } = await withSession(db, req)
for (const collection of payload.config.collections.filter(hasRelationshipOrUploadField)) {
payload.logger.info(`Migrating collection "${collection.slug}"`)
await migrateModelWithBatching({
batchSize,
config,
fields: collection.fields,
Model: db.collections[collection.slug],
session,
})
payload.logger.info(`Migrated collection "${collection.slug}"`)
if (collection.versions) {
payload.logger.info(`Migrating collection versions "${collection.slug}"`)
await migrateModelWithBatching({
batchSize,
config,
fields: buildVersionCollectionFields(config, collection),
Model: db.versions[collection.slug],
session,
})
payload.logger.info(`Migrated collection versions "${collection.slug}"`)
}
}
const { globals: GlobalsModel } = db
for (const global of payload.config.globals.filter(hasRelationshipOrUploadField)) {
payload.logger.info(`Migrating global "${global.slug}"`)
const doc = await GlobalsModel.findOne<Record<string, unknown>>(
{
globalType: {
$eq: global.slug,
},
},
{},
{ lean: true, session },
)
sanitizeRelationshipIDs({ config, data: doc, fields: global.fields })
await GlobalsModel.collection.updateOne(
{
globalType: global.slug,
},
{ $set: doc },
{ session },
)
payload.logger.info(`Migrated global "${global.slug}"`)
if (global.versions) {
payload.logger.info(`Migrating global versions "${global.slug}"`)
await migrateModelWithBatching({
batchSize,
config,
fields: buildVersionGlobalFields(config, global),
Model: db.versions[global.slug],
session,
})
payload.logger.info(`Migrated global versions "${global.slug}"`)
}
}
}

View File

@@ -1,126 +0,0 @@
import type { ClientSession } from 'mongoose'
import type { Payload, PayloadRequest } from 'payload'
import type { MongooseAdapter } from '../index.js'
import { withSession } from '../withSession.js'
export async function migrateVersionsV1_V2({ req }: { req: PayloadRequest }) {
const { payload } = req
const { session } = await withSession(payload.db as MongooseAdapter, req)
// For each collection
for (const { slug, versions } of payload.config.collections) {
if (versions?.drafts) {
await migrateCollectionDocs({ slug, payload, session })
payload.logger.info(`Migrated the "${slug}" collection.`)
}
}
// For each global
for (const { slug, versions } of payload.config.globals) {
if (versions) {
const VersionsModel = payload.db.versions[slug]
await VersionsModel.findOneAndUpdate(
{},
{ latest: true },
{
session,
sort: { updatedAt: -1 },
},
).exec()
payload.logger.info(`Migrated the "${slug}" global.`)
}
}
}
async function migrateCollectionDocs({
slug,
docsAtATime = 100,
payload,
session,
}: {
docsAtATime?: number
payload: Payload
session: ClientSession
slug: string
}) {
const VersionsModel = payload.db.versions[slug]
const remainingDocs = await VersionsModel.aggregate(
[
// Sort so that newest are first
{
$sort: {
updatedAt: -1,
},
},
// Group by parent ID
// take the $first of each
{
$group: {
_id: '$parent',
_versionID: { $first: '$_id' },
createdAt: { $first: '$createdAt' },
latest: { $first: '$latest' },
updatedAt: { $first: '$updatedAt' },
version: { $first: '$version' },
},
},
{
$match: {
latest: { $eq: null },
},
},
{
$limit: docsAtATime,
},
],
{
allowDiskUse: true,
session,
},
).exec()
if (!remainingDocs || remainingDocs.length === 0) {
const newVersions = await VersionsModel.find(
{
latest: {
$eq: true,
},
},
undefined,
{ session },
)
if (newVersions?.length) {
payload.logger.info(
`Migrated ${newVersions.length} documents in the "${slug}" versions collection.`,
)
}
return
}
const remainingDocIds = remainingDocs.map((doc) => doc._versionID)
await VersionsModel.updateMany(
{
_id: {
$in: remainingDocIds,
},
},
{
latest: true,
},
{
session,
},
)
await migrateCollectionDocs({ slug, payload, session })
}

View File

@@ -1,7 +0,0 @@
const imports = `import { migrateRelationshipsV2_V3 } from '@payloadcms/db-mongodb/migration-utils'`
const upSQL = ` await migrateRelationshipsV2_V3({
batchSize: 100,
req,
})
`
export { imports, upSQL }

View File

@@ -0,0 +1,96 @@
module.exports.up = ` async function migrateCollectionDocs(slug: string, docsAtATime = 100) {
const VersionsModel = payload.db.versions[slug]
const remainingDocs = await VersionsModel.aggregate(
[
// Sort so that newest are first
{
$sort: {
updatedAt: -1,
},
},
// Group by parent ID
// take the $first of each
{
$group: {
_id: '$parent',
_versionID: { $first: '$_id' },
createdAt: { $first: '$createdAt' },
latest: { $first: '$latest' },
updatedAt: { $first: '$updatedAt' },
version: { $first: '$version' },
},
},
{
$match: {
latest: { $eq: null },
},
},
{
$limit: docsAtATime,
},
],
{
allowDiskUse: true,
},
).exec()
if (!remainingDocs || remainingDocs.length === 0) {
const newVersions = await VersionsModel.find({
latest: {
$eq: true,
},
})
if (newVersions?.length) {
payload.logger.info(
\`Migrated \${newVersions.length} documents in the "\${slug}" versions collection.\`,
)
}
return
}
const remainingDocIds = remainingDocs.map((doc) => doc._versionID)
await VersionsModel.updateMany(
{
_id: {
$in: remainingDocIds,
},
},
{
latest: true,
},
)
await migrateCollectionDocs(slug)
}
// For each collection
await Promise.all(
payload.config.collections.map(async ({ slug, versions }) => {
if (versions?.drafts) {
return migrateCollectionDocs(slug)
}
}),
)
// For each global
await Promise.all(
payload.config.globals.map(async ({ slug, versions }) => {
if (versions) {
const VersionsModel = payload.db.versions[slug]
await VersionsModel.findOneAndUpdate(
{},
{ latest: true },
{
sort: { updatedAt: -1 },
},
).exec()
payload.logger.info(\`Migrated the "\${slug}" global.\`)
}
}),
)
`

View File

@@ -1,6 +0,0 @@
const imports = `import { migrateVersionsV1_V2 } from '@payloadcms/db-mongodb/migration-utils'`
const upSQL = ` await migrateVersionsV1_V2({
req,
})
`
export { imports, upSQL }

View File

@@ -1,6 +1,7 @@
import type { Field, Operator, PathToQuery, Payload } from 'payload'
import { Types } from 'mongoose'
import ObjectIdImport from 'bson-objectid'
import mongoose from 'mongoose'
import { getLocalizedPaths } from 'payload'
import { validOperators } from 'payload/shared'
@@ -9,6 +10,9 @@ import type { MongooseAdapter } from '../index.js'
import { operatorMap } from './operatorMap.js'
import { sanitizeQueryValue } from './sanitizeQueryValue.js'
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
type SearchParam = {
path?: string
rawQuery?: unknown
@@ -83,13 +87,13 @@ export async function buildSearchParam({
}
const [{ field, path }] = paths
if (path) {
const sanitizedQueryValue = sanitizeQueryValue({
field,
hasCustomID,
operator,
path,
payload,
val,
})
@@ -141,7 +145,7 @@ export async function buildSearchParam({
const stringID = doc._id.toString()
$in.push(stringID)
if (Types.ObjectId.isValid(stringID)) {
if (mongoose.Types.ObjectId.isValid(stringID)) {
$in.push(doc._id)
}
})
@@ -203,9 +207,9 @@ export async function buildSearchParam({
}
if (typeof formattedValue === 'string') {
if (Types.ObjectId.isValid(formattedValue)) {
if (mongoose.Types.ObjectId.isValid(formattedValue)) {
result.value[multiIDCondition].push({
[path]: { [operatorKey]: new Types.ObjectId(formattedValue) },
[path]: { [operatorKey]: ObjectId(formattedValue) },
})
} else {
;(Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]).forEach(

View File

@@ -71,10 +71,7 @@ export async function parseParams({
[searchParam.path]: searchParam.value,
}
} else if (typeof searchParam?.value === 'object') {
result = deepMergeWithCombinedArrays(result, searchParam.value, {
// dont clone Types.ObjectIDs
clone: false,
})
result = deepMergeWithCombinedArrays(result, searchParam.value)
}
}
}

View File

@@ -1,25 +1,25 @@
import type { Block, Field, Payload, RelationshipField, TabAsField } from 'payload'
import type { Field, TabAsField } from 'payload'
import { Types } from 'mongoose'
import { createArrayFromCommaDelineated, flattenTopLevelFields } from 'payload'
import ObjectIdImport from 'bson-objectid'
import mongoose from 'mongoose'
import { createArrayFromCommaDelineated } from 'payload'
type SanitizeQueryValueArgs = {
field: Field | TabAsField
hasCustomID: boolean
operator: string
path: string
payload: Payload
val: any
}
const buildExistsQuery = (formattedValue, path, treatEmptyString = true) => {
const buildExistsQuery = (formattedValue, path) => {
if (formattedValue) {
return {
rawQuery: {
$and: [
{ [path]: { $exists: true } },
{ [path]: { $ne: null } },
...(treatEmptyString ? [{ [path]: { $ne: '' } }] : []), // Treat empty string as null / undefined
{ [path]: { $ne: '' } }, // Exclude null and empty string
],
},
}
@@ -29,56 +29,20 @@ const buildExistsQuery = (formattedValue, path, treatEmptyString = true) => {
$or: [
{ [path]: { $exists: false } },
{ [path]: { $eq: null } },
...(treatEmptyString ? [{ [path]: { $eq: '' } }] : []), // Treat empty string as null / undefined
{ [path]: { $eq: '' } }, // Treat empty string as null / undefined
],
},
}
}
}
// returns nestedField Field object from blocks.nestedField path because getLocalizedPaths splits them only for relationships
const getFieldFromSegments = ({
field,
segments,
}: {
field: Block | Field | TabAsField
segments: string[]
}) => {
if ('blocks' in field) {
for (const block of field.blocks) {
const field = getFieldFromSegments({ field: block, segments })
if (field) {
return field
}
}
}
if ('fields' in field) {
for (let i = 0; i < segments.length; i++) {
const foundField = flattenTopLevelFields(field.fields).find(
(each) => each.name === segments[i],
)
if (!foundField) {
break
}
if (foundField && segments.length - 1 === i) {
return foundField
}
segments.shift()
return getFieldFromSegments({ field: foundField, segments })
}
}
}
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
export const sanitizeQueryValue = ({
field,
hasCustomID,
operator,
path,
payload,
val,
}: SanitizeQueryValueArgs): {
operator?: string
@@ -88,31 +52,21 @@ export const sanitizeQueryValue = ({
let formattedValue = val
let formattedOperator = operator
if (['array', 'blocks', 'group', 'tab'].includes(field.type) && path.includes('.')) {
const segments = path.split('.')
segments.shift()
const foundField = getFieldFromSegments({ field, segments })
if (foundField) {
field = foundField
}
}
// Disregard invalid _ids
if (path === '_id') {
if (typeof val === 'string' && val.split(',').length === 1) {
if (!hasCustomID) {
const isValid = Types.ObjectId.isValid(val)
const isValid = mongoose.Types.ObjectId.isValid(val)
if (!isValid) {
return { operator: formattedOperator, val: undefined }
} else {
if (['in', 'not_in'].includes(operator)) {
formattedValue = createArrayFromCommaDelineated(formattedValue).map(
(id) => new Types.ObjectId(id),
formattedValue = createArrayFromCommaDelineated(formattedValue).map((id) =>
ObjectId(id),
)
} else {
formattedValue = new Types.ObjectId(val)
formattedValue = ObjectId(val)
}
}
}
@@ -130,22 +84,21 @@ export const sanitizeQueryValue = ({
}
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
const newValues = [inVal]
if (!hasCustomID) {
if (Types.ObjectId.isValid(inVal)) {
formattedValues.push(new Types.ObjectId(inVal))
if (mongoose.Types.ObjectId.isValid(inVal)) {
newValues.push(ObjectId(inVal))
}
}
if (field.type === 'number') {
const parsedNumber = parseFloat(inVal)
if (!Number.isNaN(parsedNumber)) {
formattedValues.push(parsedNumber)
newValues.push(parsedNumber)
}
} else {
formattedValues.push(inVal)
}
return formattedValues
return [...formattedValues, ...newValues]
}, [])
}
}
@@ -201,10 +154,10 @@ export const sanitizeQueryValue = ({
formattedValue.relationTo
) {
const { value } = formattedValue
const isValid = Types.ObjectId.isValid(value)
const isValid = mongoose.Types.ObjectId.isValid(value)
if (isValid) {
formattedValue.value = new Types.ObjectId(value)
formattedValue.value = ObjectId(value)
}
return {
@@ -217,88 +170,25 @@ export const sanitizeQueryValue = ({
}
}
const relationTo = (field as RelationshipField).relationTo
if (['in', 'not_in'].includes(operator) && Array.isArray(formattedValue)) {
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
if (!inVal) {
return formattedValues
const newValues = [inVal]
if (mongoose.Types.ObjectId.isValid(inVal)) {
newValues.push(ObjectId(inVal))
}
if (typeof relationTo === 'string' && payload.collections[relationTo].customIDType) {
if (payload.collections[relationTo].customIDType === 'number') {
const parsedNumber = parseFloat(inVal)
if (!Number.isNaN(parsedNumber)) {
formattedValues.push(parsedNumber)
return formattedValues
}
}
formattedValues.push(inVal)
return formattedValues
const parsedNumber = parseFloat(inVal)
if (!Number.isNaN(parsedNumber)) {
newValues.push(parsedNumber)
}
if (
Array.isArray(relationTo) &&
relationTo.some((relationTo) => !!payload.collections[relationTo].customIDType)
) {
if (Types.ObjectId.isValid(inVal.toString())) {
formattedValues.push(new Types.ObjectId(inVal))
} else {
formattedValues.push(inVal)
}
return formattedValues
}
if (Types.ObjectId.isValid(inVal.toString())) {
formattedValues.push(new Types.ObjectId(inVal))
}
return formattedValues
return [...formattedValues, ...newValues]
}, [])
}
if (
['contains', 'equals', 'like', 'not_equals'].includes(operator) &&
(!Array.isArray(relationTo) || !path.endsWith('.relationTo'))
) {
if (typeof relationTo === 'string') {
const customIDType = payload.collections[relationTo].customIDType
if (customIDType) {
if (customIDType === 'number') {
formattedValue = parseFloat(val)
if (Number.isNaN(formattedValue)) {
return { operator: formattedOperator, val: undefined }
}
}
} else {
if (!Types.ObjectId.isValid(formattedValue)) {
return { operator: formattedOperator, val: undefined }
}
formattedValue = new Types.ObjectId(formattedValue)
}
} else {
const hasCustomIDType = relationTo.some(
(relationTo) => !!payload.collections[relationTo].customIDType,
)
if (hasCustomIDType) {
if (typeof val === 'string') {
const formattedNumber = Number(val)
formattedValue = [Types.ObjectId.isValid(val) ? new Types.ObjectId(val) : val]
formattedOperator = operator === 'not_equals' ? 'not_in' : 'in'
if (!Number.isNaN(formattedNumber)) {
formattedValue.push(formattedNumber)
}
}
} else {
if (!Types.ObjectId.isValid(formattedValue)) {
return { operator: formattedOperator, val: undefined }
}
formattedValue = new Types.ObjectId(formattedValue)
}
if (operator === 'contains' && typeof formattedValue === 'string') {
if (mongoose.Types.ObjectId.isValid(formattedValue)) {
formattedValue = ObjectId(formattedValue)
}
}
}
@@ -342,7 +232,7 @@ export const sanitizeQueryValue = ({
}
if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) {
if (operator === 'contains' && !Types.ObjectId.isValid(formattedValue)) {
if (operator === 'contains' && !mongoose.Types.ObjectId.isValid(formattedValue)) {
formattedValue = {
$options: 'i',
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
@@ -352,12 +242,7 @@ export const sanitizeQueryValue = ({
if (operator === 'exists') {
formattedValue = formattedValue === 'true' || formattedValue === true
// _id can't be empty string, will error Cast to ObjectId failed for value ""
return buildExistsQuery(
formattedValue,
path,
!['relationship', 'upload'].includes(field.type),
)
return buildExistsQuery(formattedValue, path)
}
}

View File

@@ -65,6 +65,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
const useEstimatedCount =
hasNearConstraint || !versionQuery || Object.keys(versionQuery).length === 0
const paginationOptions: PaginateOptions = {
forceCountFn: hasNearConstraint,
lean: true,
leanWithId: true,
options,

View File

@@ -1,4 +1,3 @@
import type { QueryOptions } from 'mongoose'
import type { PayloadRequest, UpdateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -10,13 +9,12 @@ import { withSession } from './withSession.js'
export const updateGlobal: UpdateGlobal = async function updateGlobal(
this: MongooseAdapter,
{ slug, data, options: optionsArgs = {}, req = {} as PayloadRequest, select },
{ slug, data, req = {} as PayloadRequest, select },
) {
const Model = this.globals
const fields = this.payload.config.globals.find((global) => global.slug === slug).fields
const options: QueryOptions = {
...optionsArgs,
const options = {
...(await withSession(this, req)),
lean: true,
new: true,

View File

@@ -1,5 +1,3 @@
import type { QueryOptions } from 'mongoose'
import {
buildVersionGlobalFields,
type PayloadRequest,
@@ -19,7 +17,6 @@ export async function updateGlobalVersion<T extends TypeWithID>(
id,
global: globalSlug,
locale,
options: optionsArgs = {},
req = {} as PayloadRequest,
select,
versionData,
@@ -33,8 +30,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
this.payload.config.globals.find((global) => global.slug === globalSlug),
)
const options: QueryOptions = {
...optionsArgs,
const options = {
...(await withSession(this, req)),
lean: true,
new: true,

View File

@@ -1,5 +1,3 @@
import type { QueryOptions } from 'mongoose'
import { buildVersionCollectionFields, type PayloadRequest, type UpdateVersion } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -10,16 +8,7 @@ import { withSession } from './withSession.js'
export const updateVersion: UpdateVersion = async function updateVersion(
this: MongooseAdapter,
{
id,
collection,
locale,
options: optionsArgs = {},
req = {} as PayloadRequest,
select,
versionData,
where,
},
{ id, collection, locale, req = {} as PayloadRequest, select, versionData, where },
) {
const VersionModel = this.versions[collection]
const whereToUse = where || { id: { equals: id } }
@@ -28,8 +17,7 @@ export const updateVersion: UpdateVersion = async function updateVersion(
this.payload.collections[collection].config,
)
const options: QueryOptions = {
...optionsArgs,
const options = {
...(await withSession(this, req)),
lean: true,
new: true,

View File

@@ -1,344 +0,0 @@
import type { Field, SanitizedConfig } from 'payload'
import { Types } from 'mongoose'
import { sanitizeRelationshipIDs } from './sanitizeRelationshipIDs.js'
const flattenRelationshipValues = (obj: Record<string, any>, prefix = ''): Record<string, any> => {
return Object.keys(obj).reduce(
(acc, key) => {
const fullKey = prefix ? `${prefix}.${key}` : key
const value = obj[key]
if (value && typeof value === 'object' && !(value instanceof Types.ObjectId)) {
Object.assign(acc, flattenRelationshipValues(value, fullKey))
// skip relationTo and blockType
} else if (!fullKey.endsWith('relationTo') && !fullKey.endsWith('blockType')) {
acc[fullKey] = value
}
return acc
},
{} as Record<string, any>,
)
}
const relsFields: Field[] = [
{
name: 'rel_1',
type: 'relationship',
relationTo: 'rels',
},
{
name: 'rel_1_l',
type: 'relationship',
localized: true,
relationTo: 'rels',
},
{
name: 'rel_2',
type: 'relationship',
hasMany: true,
relationTo: 'rels',
},
{
name: 'rel_2_l',
type: 'relationship',
hasMany: true,
localized: true,
relationTo: 'rels',
},
{
name: 'rel_3',
type: 'relationship',
relationTo: ['rels'],
},
{
name: 'rel_3_l',
type: 'relationship',
localized: true,
relationTo: ['rels'],
},
{
name: 'rel_4',
type: 'relationship',
hasMany: true,
relationTo: ['rels'],
},
{
name: 'rel_4_l',
type: 'relationship',
hasMany: true,
localized: true,
relationTo: ['rels'],
},
]
const config = {
collections: [
{
slug: 'docs',
fields: [
...relsFields,
{
name: 'array',
type: 'array',
fields: [
{
name: 'array',
type: 'array',
fields: relsFields,
},
{
name: 'blocks',
type: 'blocks',
blocks: [{ slug: 'block', fields: relsFields }],
},
...relsFields,
],
},
{
name: 'arrayLocalized',
type: 'array',
fields: [
{
name: 'array',
type: 'array',
fields: relsFields,
},
{
name: 'blocks',
type: 'blocks',
blocks: [{ slug: 'block', fields: relsFields }],
},
...relsFields,
],
localized: true,
},
{
name: 'blocks',
type: 'blocks',
blocks: [
{
slug: 'block',
fields: [
...relsFields,
{
name: 'group',
type: 'group',
fields: relsFields,
},
{
name: 'array',
type: 'array',
fields: relsFields,
},
],
},
],
},
{
name: 'group',
type: 'group',
fields: [
...relsFields,
{
name: 'array',
type: 'array',
fields: relsFields,
},
],
},
{
name: 'groupLocalized',
type: 'group',
fields: [
...relsFields,
{
name: 'array',
type: 'array',
fields: relsFields,
},
],
localized: true,
},
{
name: 'groupAndRow',
type: 'group',
fields: [
{
type: 'row',
fields: [
...relsFields,
{
type: 'array',
name: 'array',
fields: relsFields,
},
],
},
],
},
{
type: 'tabs',
tabs: [
{
name: 'tab',
fields: relsFields,
},
{
name: 'tabLocalized',
fields: relsFields,
localized: true,
},
],
},
],
},
{
slug: 'rels',
fields: [],
},
],
localization: {
defaultLocale: 'en',
localeCodes: ['en', 'es'],
locales: [
{ code: 'en', label: 'EN' },
{ code: 'es', label: 'ES' },
],
},
} as SanitizedConfig
const relsData = {
rel_1: new Types.ObjectId().toHexString(),
rel_1_l: {
en: new Types.ObjectId().toHexString(),
es: new Types.ObjectId().toHexString(),
},
rel_2: [new Types.ObjectId().toHexString()],
rel_2_l: {
en: [new Types.ObjectId().toHexString()],
es: [new Types.ObjectId().toHexString()],
},
rel_3: {
relationTo: 'rels',
value: new Types.ObjectId().toHexString(),
},
rel_3_l: {
en: {
relationTo: 'rels',
value: new Types.ObjectId().toHexString(),
},
es: {
relationTo: 'rels',
value: new Types.ObjectId().toHexString(),
},
},
rel_4: [
{
relationTo: 'rels',
value: new Types.ObjectId().toHexString(),
},
],
rel_4_l: {
en: [
{
relationTo: 'rels',
value: new Types.ObjectId().toHexString(),
},
],
es: [
{
relationTo: 'rels',
value: new Types.ObjectId().toHexString(),
},
],
},
}
describe('sanitizeRelationshipIDs', () => {
it('should sanitize relationships', () => {
const data = {
...relsData,
array: [
{
...relsData,
array: [{ ...relsData }],
blocks: [
{
blockType: 'block',
...relsData,
},
],
},
],
arrayLocalized: {
en: [
{
...relsData,
array: [{ ...relsData }],
blocks: [
{
blockType: 'block',
...relsData,
},
],
},
],
es: [
{
...relsData,
array: [{ ...relsData }],
blocks: [
{
blockType: 'block',
...relsData,
},
],
},
],
},
blocks: [
{
blockType: 'block',
...relsData,
array: [{ ...relsData }],
group: { ...relsData },
},
],
group: {
...relsData,
array: [{ ...relsData }],
},
groupAndRow: {
...relsData,
array: [{ ...relsData }],
},
groupLocalized: {
en: {
...relsData,
array: [{ ...relsData }],
},
es: {
...relsData,
array: [{ ...relsData }],
},
},
tab: { ...relsData },
tabLocalized: {
en: { ...relsData },
es: { ...relsData },
},
}
const flattenValuesBefore = Object.values(flattenRelationshipValues(data))
sanitizeRelationshipIDs({ config, data, fields: config.collections[0].fields })
const flattenValuesAfter = Object.values(flattenRelationshipValues(data))
flattenValuesAfter.forEach((value, i) => {
expect(value).toBeInstanceOf(Types.ObjectId)
expect(flattenValuesBefore[i]).toBe(value.toHexString())
})
})
})

View File

@@ -1,6 +1,6 @@
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
import { Types } from 'mongoose'
import mongoose from 'mongoose'
import { APIError, traverseFields } from 'payload'
import { fieldAffectsData } from 'payload/shared'
@@ -25,14 +25,14 @@ const convertValue = ({
}: {
relatedCollection: CollectionConfig
value: number | string
}): number | string | Types.ObjectId => {
}): mongoose.Types.ObjectId | number | string => {
const customIDField = relatedCollection.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (!customIDField) {
try {
return new Types.ObjectId(value)
return new mongoose.Types.ObjectId(value)
} catch (error) {
throw new APIError(
`Failed to create ObjectId from value: ${value}. Error: ${error.message}`,
@@ -141,7 +141,7 @@ export const sanitizeRelationshipIDs = ({
}
}
traverseFields({ callback: sanitize, fields, fillEmpty: false, ref: data })
traverseFields({ callback: sanitize, fields, ref: data })
return data
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"description": "The official Vue SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,9 +1,9 @@
import type { I18n } from '@payloadcms/translations'
import type {
Payload,
Permissions,
SanitizedCollectionConfig,
SanitizedGlobalConfig,
SanitizedPermissions,
} from 'payload'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
@@ -23,7 +23,7 @@ export const DocumentTabs: React.FC<{
globalConfig: SanitizedGlobalConfig
i18n: I18n
payload: Payload
permissions: SanitizedPermissions
permissions: Permissions
}> = (props) => {
const { collectionConfig, globalConfig, i18n, payload, permissions } = props
const { config } = payload

View File

@@ -72,8 +72,9 @@ export const tabs: Record<
condition: ({ collectionConfig, globalConfig, permissions }) =>
Boolean(
(collectionConfig?.versions &&
permissions?.collections?.[collectionConfig?.slug]?.readVersions) ||
(globalConfig?.versions && permissions?.globals?.[globalConfig?.slug]?.readVersions),
permissions?.collections?.[collectionConfig?.slug]?.readVersions?.permission) ||
(globalConfig?.versions &&
permissions?.globals?.[globalConfig?.slug]?.readVersions?.permission),
),
href: '/versions',
label: ({ t }) => t('version:versions'),

View File

@@ -1,9 +1,9 @@
import type { I18n } from '@payloadcms/translations'
import type {
Payload,
Permissions,
SanitizedCollectionConfig,
SanitizedGlobalConfig,
SanitizedPermissions,
} from 'payload'
import { Gutter, RenderTitle } from '@payloadcms/ui'
@@ -20,7 +20,7 @@ export const DocumentHeader: React.FC<{
hideTabs?: boolean
i18n: I18n
payload: Payload
permissions: SanitizedPermissions
permissions: Permissions
}> = (props) => {
const { collectionConfig, globalConfig, hideTabs, i18n, payload, permissions } = props

View File

@@ -1,5 +1,5 @@
import type { I18n, I18nClient } from '@payloadcms/translations'
import type { PayloadRequest, SanitizedConfig, SanitizedPermissions, User } from 'payload'
import type { PayloadRequest, Permissions, SanitizedConfig, User } from 'payload'
import { initI18n } from '@payloadcms/translations'
import { headers as getHeaders } from 'next/headers.js'
@@ -11,7 +11,7 @@ import { getRequestLanguage } from './getRequestLanguage.js'
type Result = {
i18n: I18nClient
permissions: SanitizedPermissions
permissions: Permissions
req: PayloadRequest
user: User
}

View File

@@ -2,10 +2,10 @@
import type { FormProps, UserWithToken } from '@payloadcms/ui'
import type {
ClientCollectionConfig,
DocumentPermissions,
DocumentPreferences,
FormState,
LoginWithUsernameOptions,
SanitizedDocumentPermissions,
} from 'payload'
import {
@@ -24,7 +24,7 @@ import { abortAndIgnore } from '@payloadcms/ui/shared'
import React, { useEffect } from 'react'
export const CreateFirstUserClient: React.FC<{
docPermissions: SanitizedDocumentPermissions
docPermissions: DocumentPermissions
docPreferences: DocumentPreferences
initialState: FormState
loginWithUsername?: false | LoginWithUsernameOptions
@@ -114,7 +114,7 @@ export const CreateFirstUserClient: React.FC<{
parentIndexPath=""
parentPath=""
parentSchemaPath={userSlug}
permissions={true}
permissions={null}
readOnly={false}
/>
<FormSubmit size="large">{t('general:create')}</FormSubmit>

View File

@@ -1,5 +1,5 @@
import type { groupNavItems } from '@payloadcms/ui/shared'
import type { ClientUser, SanitizedPermissions, ServerProps, VisibleEntities } from 'payload'
import type { ClientUser, Permissions, ServerProps, VisibleEntities } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { Button, Card, Gutter, Locked } from '@payloadcms/ui'
@@ -19,7 +19,7 @@ export type DashboardProps = {
}>
Link: React.ComponentType<any>
navGroups?: ReturnType<typeof groupNavItems>
permissions: SanitizedPermissions
permissions: Permissions
visibleEntities: VisibleEntities
} & ServerProps
@@ -94,7 +94,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
path: `/collections/${slug}/create`,
})
hasCreatePermission = permissions?.collections?.[slug]?.create
hasCreatePermission = permissions?.collections?.[slug]?.create?.permission
}
if (type === EntityType.global) {

View File

@@ -35,13 +35,14 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
const collections = config.collections.filter(
(collection) =>
permissions?.collections?.[collection.slug]?.read &&
permissions?.collections?.[collection.slug]?.read?.permission &&
visibleEntities.collections.includes(collection.slug),
)
const globals = config.globals.filter(
(global) =>
permissions?.globals?.[global.slug]?.read && visibleEntities.globals.includes(global.slug),
permissions?.globals?.[global.slug]?.read?.permission &&
visibleEntities.globals.includes(global.slug),
)
// Query locked global documents only if there are globals in the config

View File

@@ -3,7 +3,6 @@ import type {
DocumentPermissions,
PayloadRequest,
SanitizedCollectionConfig,
SanitizedDocumentPermissions,
SanitizedGlobalConfig,
} from 'payload'
@@ -11,7 +10,7 @@ import {
hasSavePermission as getHasSavePermission,
isEditing as getIsEditing,
} from '@payloadcms/ui/shared'
import { docAccessOperation, docAccessOperationGlobal, sanitizePermissions } from 'payload'
import { docAccessOperation, docAccessOperationGlobal } from 'payload'
export const getDocumentPermissions = async (args: {
collectionConfig?: SanitizedCollectionConfig
@@ -20,7 +19,7 @@ export const getDocumentPermissions = async (args: {
id?: number | string
req: PayloadRequest
}): Promise<{
docPermissions: SanitizedDocumentPermissions
docPermissions: DocumentPermissions
hasPublishPermission: boolean
hasSavePermission: boolean
}> => {
@@ -92,13 +91,9 @@ export const getDocumentPermissions = async (args: {
}
}
// TODO: do this in a better way. Only doing this bc this is how the fn was written (mutates the original object)
const sanitizedDocPermissions = { ...docPermissions } as any as SanitizedDocumentPermissions
sanitizePermissions(sanitizedDocPermissions)
const hasSavePermission = getHasSavePermission({
collectionSlug: collectionConfig?.slug,
docPermissions: sanitizedDocPermissions,
docPermissions,
globalSlug: globalConfig?.slug,
isEditing: getIsEditing({
id,
@@ -108,7 +103,7 @@ export const getDocumentPermissions = async (args: {
})
return {
docPermissions: sanitizedDocPermissions,
docPermissions,
hasPublishPermission,
hasSavePermission,
}

View File

@@ -1,14 +1,14 @@
import type {
DocumentPermissions,
Payload,
SanitizedCollectionConfig,
SanitizedDocumentPermissions,
SanitizedGlobalConfig,
TypedUser,
} from 'payload'
type Args = {
collectionConfig?: SanitizedCollectionConfig
docPermissions: SanitizedDocumentPermissions
docPermissions: DocumentPermissions
globalConfig?: SanitizedGlobalConfig
id?: number | string
locale?: string
@@ -43,7 +43,7 @@ export const getVersions = async ({
const entityConfig = collectionConfig || globalConfig
const versionsConfig = entityConfig?.versions
const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions)
const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions?.permission)
if (!shouldFetchVersions) {
const hasPublishedDoc = Boolean((collectionConfig && id) || globalConfig)

View File

@@ -1,11 +1,11 @@
import type {
AdminViewProps,
CollectionPermission,
GlobalPermission,
PayloadComponent,
SanitizedCollectionConfig,
SanitizedCollectionPermission,
SanitizedConfig,
SanitizedGlobalConfig,
SanitizedGlobalPermission,
ServerSideEditViewProps,
} from 'payload'
import type React from 'react'
@@ -38,7 +38,7 @@ export const getViewsFromConfig = ({
routeSegments: string[]
} & (
| {
docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission
docPermissions: CollectionPermission | GlobalPermission
overrideDocPermissions?: false | undefined
}
| {
@@ -78,7 +78,7 @@ export const getViewsFromConfig = ({
const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] =
routeSegments
if (!overrideDocPermissions && !docPermissions?.read) {
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
throw new Error('not-found')
} else {
// `../:id`, or `../create`
@@ -86,7 +86,11 @@ export const getViewsFromConfig = ({
case 3: {
switch (segment3) {
case 'create': {
if (!overrideDocPermissions && 'create' in docPermissions && docPermissions.create) {
if (
!overrideDocPermissions &&
'create' in docPermissions &&
docPermissions?.create?.permission
) {
CustomView = {
ComponentConfig: getCustomViewByKey(views, 'default'),
}
@@ -172,7 +176,7 @@ export const getViewsFromConfig = ({
}
case 'versions': {
if (!overrideDocPermissions && docPermissions?.readVersions) {
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
CustomView = {
ComponentConfig: getCustomViewByKey(views, 'versions'),
}
@@ -225,7 +229,7 @@ export const getViewsFromConfig = ({
// `../:id/versions/:version`, etc
default: {
if (segment4 === 'versions') {
if (!overrideDocPermissions && docPermissions?.readVersions) {
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
CustomView = {
ComponentConfig: getCustomViewByKey(views, 'version'),
}
@@ -277,7 +281,7 @@ export const getViewsFromConfig = ({
if (globalConfig) {
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
if (!overrideDocPermissions && !docPermissions?.read) {
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
throw new Error('not-found')
} else {
switch (routeSegments.length) {
@@ -319,7 +323,7 @@ export const getViewsFromConfig = ({
}
case 'versions': {
if (!overrideDocPermissions && docPermissions?.readVersions) {
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
CustomView = {
ComponentConfig: getCustomViewByKey(views, 'versions'),
}
@@ -336,7 +340,7 @@ export const getViewsFromConfig = ({
}
default: {
if (!overrideDocPermissions && docPermissions?.read) {
if (!overrideDocPermissions && docPermissions?.read?.permission) {
const baseRoute = [adminRoute, globalEntity, globalSlug, segment3]
.filter(Boolean)
.join('/')
@@ -377,7 +381,7 @@ export const getViewsFromConfig = ({
default: {
// `../:slug/versions/:version`, etc
if (segment3 === 'versions') {
if (!overrideDocPermissions && docPermissions?.readVersions) {
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
CustomView = {
ComponentConfig: getCustomViewByKey(views, 'version'),
}

View File

@@ -1,9 +1,9 @@
import type {
DefaultServerFunctionArgs,
DocumentPermissions,
DocumentSlots,
PayloadRequest,
SanitizedCollectionConfig,
SanitizedDocumentPermissions,
SanitizedGlobalConfig,
StaticDescription,
} from 'payload'
@@ -18,7 +18,7 @@ export const renderDocumentSlots: (args: {
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
hasSavePermission: boolean
permissions: SanitizedDocumentPermissions
permissions: DocumentPermissions
req: PayloadRequest
}) => DocumentSlots = (args) => {
const { collectionConfig, globalConfig, hasSavePermission, req } = args

View File

@@ -66,7 +66,7 @@ export const renderListView = async (
visibleEntities,
} = initPageResult
if (!permissions?.collections?.[collectionSlug]?.read) {
if (!permissions?.collections?.[collectionSlug]?.read?.permission) {
throw new Error('not-found')
}
@@ -190,7 +190,7 @@ export const renderListView = async (
const sharedClientProps: ListComponentClientProps = {
collectionSlug,
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create,
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission,
newDocumentURL: formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}/create`,

View File

@@ -65,7 +65,7 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
const comparison = compareValue?.value && currentComparisonDoc?.version // the `version` key is only present on `versions` documents
const canUpdate = docPermissions?.update
const canUpdate = docPermissions?.update?.permission
const localeValues = locales && locales.map((locale) => locale.value)

View File

@@ -1,9 +1,4 @@
import type {
Document,
OptionObject,
SanitizedCollectionPermission,
SanitizedGlobalPermission,
} from 'payload'
import type { CollectionPermission, Document, GlobalPermission, OptionObject } from 'payload'
export type CompareOption = {
label: React.ReactNode | string
@@ -14,7 +9,7 @@ export type CompareOption = {
export type DefaultVersionsViewProps = {
readonly doc: Document
readonly docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission
readonly docPermissions: CollectionPermission | GlobalPermission
readonly initialComparisonDoc: Document
readonly latestDraftVersion?: string
readonly latestPublishedVersion?: string

View File

@@ -1,5 +1,5 @@
import type { I18nClient } from '@payloadcms/translations'
import type { ClientField, SanitizedFieldPermissions } from 'payload'
import type { ClientField, FieldPermissions } from 'payload'
import type React from 'react'
import type { DiffMethod } from 'react-diff-viewer-continued'
@@ -16,10 +16,6 @@ export type DiffComponentProps<TField extends ClientField = ClientField> = {
readonly isRichText?: boolean
readonly locale?: string
readonly locales?: string[]
readonly permissions?:
| {
[key: string]: SanitizedFieldPermissions
}
| true
readonly permissions?: Record<string, FieldPermissions>
readonly version: any
}

View File

@@ -50,13 +50,11 @@ const RenderFieldsToDiff: React.FC<Props> = ({
? JSON.stringify(comparison?.[fieldName])
: comparison?.[fieldName]
const hasPermission =
fieldPermissions?.[fieldName] === true || fieldPermissions?.[fieldName]?.read
const hasPermission = fieldPermissions?.[fieldName]?.read?.permission
const subFieldPermissions =
fieldPermissions?.[fieldName] === true || fieldPermissions?.[fieldName]?.fields
const subFieldPermissions = fieldPermissions?.[fieldName]?.fields
if (!hasPermission) {
if (hasPermission === false) {
return null
}

View File

@@ -1,5 +1,5 @@
import type { I18nClient } from '@payloadcms/translations'
import type { ClientField, SanitizedFieldPermissions } from 'payload'
import type { ClientField, FieldPermissions } from 'payload'
import type { DiffMethod } from 'react-diff-viewer-continued'
import type { DiffComponents } from './fields/types.js'
@@ -7,11 +7,7 @@ import type { DiffComponents } from './fields/types.js'
export type Props = {
readonly comparison: Record<string, any>
readonly diffComponents: DiffComponents
readonly fieldPermissions:
| {
[key: string]: SanitizedFieldPermissions
}
| true
readonly fieldPermissions: Record<string, FieldPermissions>
readonly fields: ClientField[]
readonly i18n: I18nClient
readonly locales: string[]

View File

@@ -1,10 +1,10 @@
import type {
CollectionPermission,
Document,
EditViewComponent,
GlobalPermission,
OptionObject,
PayloadServerReactComponent,
SanitizedCollectionPermission,
SanitizedGlobalPermission,
} from 'payload'
import { notFound } from 'next/navigation.js'
@@ -33,7 +33,7 @@ export const VersionView: PayloadServerReactComponent<EditViewComponent> = async
const { localization } = config
let docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission
let docPermissions: CollectionPermission | GlobalPermission
let slug: string
let doc: Document

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload-cloud",
"version": "3.0.0-beta.131",
"version": "3.0.0-beta.130",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

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