Compare commits
36 Commits
feat/beta/
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90686fa50a | ||
|
|
26ffbca914 | ||
|
|
0b9d5a5ae4 | ||
|
|
0f7276e3c4 | ||
|
|
68458787a5 | ||
|
|
810c29b189 | ||
|
|
a5cae077cc | ||
|
|
ba06ce6338 | ||
|
|
7c732bec14 | ||
|
|
7c6f41936b | ||
|
|
028153f5a4 | ||
|
|
2801c41d91 | ||
|
|
82e72fa7f2 | ||
|
|
20c899286e | ||
|
|
729488028b | ||
|
|
e6e0cc2a63 | ||
|
|
2d2d020c29 | ||
|
|
315b4e566b | ||
|
|
2d7626c3e9 | ||
|
|
e75527b0a1 | ||
|
|
5482e7ea15 | ||
|
|
77c99c2f49 | ||
|
|
5ff1bb366c | ||
|
|
e6d04436a8 | ||
|
|
81099cbb04 | ||
|
|
4509c38f4c | ||
|
|
90e6a4fcd8 | ||
|
|
4690cd819a | ||
|
|
afd69c4d54 | ||
|
|
de52490a98 | ||
|
|
129fadfd2c | ||
|
|
cea7d58d96 | ||
|
|
6baff8a3ba | ||
|
|
ced79be591 | ||
|
|
5b9cee67c0 | ||
|
|
bcbca0e44a |
1
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
1
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
@@ -39,6 +39,7 @@ body:
|
||||
- 'db-postgres'
|
||||
- 'db-sqlite'
|
||||
- 'db-vercel-postgres'
|
||||
- 'email-nodemailer'
|
||||
- 'plugin: cloud'
|
||||
- 'plugin: cloud-storage'
|
||||
- 'plugin: form-builder'
|
||||
|
||||
@@ -21,10 +21,11 @@ To do so, import the `useField` hook as follows:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import type { TextFieldClientComponent } from 'payload'
|
||||
import { useField } from '@payloadcms/ui'
|
||||
|
||||
const CustomTextField: React.FC = () => {
|
||||
const { value, setValue, path } = useField() // highlight-line
|
||||
export const CustomTextField: TextFieldClientComponent = ({ path }) => {
|
||||
const { value, setValue } = useField({ path }) // highlight-line
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -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.
|
||||
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).
|
||||
|
||||
Here is one of the simplest possible Payload configs:
|
||||
|
||||
|
||||
@@ -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/v2.29.3/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/v4.1.0/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`.
|
||||
|
||||
|
||||
@@ -73,6 +73,59 @@ export const ExampleCollection: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
## Querying
|
||||
## Querying - near
|
||||
|
||||
In order to do query based on the distance to another point, you can use the `near` operator. When querying using the near operator, the returned documents will be sorted by nearest first.
|
||||
|
||||
## Querying - within
|
||||
|
||||
In order to do query based on whether points are within a specific area defined in GeoJSON, you can use the `within` operator.
|
||||
Example:
|
||||
```ts
|
||||
const polygon: Point[] = [
|
||||
[9.0, 19.0], // bottom-left
|
||||
[9.0, 21.0], // top-left
|
||||
[11.0, 21.0], // top-right
|
||||
[11.0, 19.0], // bottom-right
|
||||
[9.0, 19.0], // back to starting point to close the polygon
|
||||
]
|
||||
|
||||
payload.find({
|
||||
collection: "points",
|
||||
where: {
|
||||
point: {
|
||||
within: {
|
||||
type: 'Polygon',
|
||||
coordinates: [polygon],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Querying - intersects
|
||||
|
||||
In order to do query based on whether points intersect a specific area defined in GeoJSON, you can use the `intersects` operator.
|
||||
Example:
|
||||
```ts
|
||||
const polygon: Point[] = [
|
||||
[9.0, 19.0], // bottom-left
|
||||
[9.0, 21.0], // top-left
|
||||
[11.0, 21.0], // top-right
|
||||
[11.0, 19.0], // bottom-right
|
||||
[9.0, 19.0], // back to starting point to close the polygon
|
||||
]
|
||||
|
||||
payload.find({
|
||||
collection: "points",
|
||||
where: {
|
||||
point: {
|
||||
intersects: {
|
||||
type: 'Polygon',
|
||||
coordinates: [polygon],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -97,6 +97,17 @@ You can specify more options within the Local API vs. REST or GraphQL due to the
|
||||
|
||||
_There are more options available on an operation by operation basis outlined below._
|
||||
|
||||
## Transactions
|
||||
|
||||
When your database uses transactions you need to thread req through to all local operations. Postgres uses transactions and MongoDB uses transactions when you are using replica sets. Passing req without transactions is still recommended.
|
||||
|
||||
```js
|
||||
const post = await payload.find({
|
||||
collection: 'posts',
|
||||
req, // passing req is recommended
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -37,21 +37,23 @@ _The exact query syntax will depend on the API you are using, but the concepts a
|
||||
|
||||
The following operators are available for use in queries:
|
||||
|
||||
| Operator | Description |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `equals` | The value must be exactly equal. |
|
||||
| `not_equals` | The query will return all documents where the value is not equal. |
|
||||
| `greater_than` | For numeric or date-based fields. |
|
||||
| `greater_than_equal` | For numeric or date-based fields. |
|
||||
| `less_than` | For numeric or date-based fields. |
|
||||
| `less_than_equal` | For numeric or date-based fields. |
|
||||
| `like` | Case-insensitive string must be present. If string of words, all words must be present, in any order. |
|
||||
| `contains` | Must contain the value entered, case-insensitive. |
|
||||
| `in` | The value must be found within the provided comma-delimited list of values. |
|
||||
| `not_in` | The value must NOT be within the provided comma-delimited list of values. |
|
||||
| `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)>`. |
|
||||
| Operator | Description |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `equals` | The value must be exactly equal. |
|
||||
| `not_equals` | The query will return all documents where the value is not equal. |
|
||||
| `greater_than` | For numeric or date-based fields. |
|
||||
| `greater_than_equal` | For numeric or date-based fields. |
|
||||
| `less_than` | For numeric or date-based fields. |
|
||||
| `less_than_equal` | For numeric or date-based fields. |
|
||||
| `like` | Case-insensitive string must be present. If string of words, all words must be present, in any order. |
|
||||
| `contains` | Must contain the value entered, case-insensitive. |
|
||||
| `in` | The value must be found within the provided comma-delimited list of values. |
|
||||
| `not_in` | The value must NOT be within the provided comma-delimited list of values. |
|
||||
| `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) |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
|
||||
@@ -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.slug]: true,
|
||||
[MediaWithPrefix.slug]: {
|
||||
media: true,
|
||||
'media-with-prefix': {
|
||||
prefix: 'my-prefix',
|
||||
},
|
||||
},
|
||||
@@ -90,8 +90,8 @@ export default buildConfig({
|
||||
plugins: [
|
||||
s3Storage({
|
||||
collections: {
|
||||
[mediaSlug]: true,
|
||||
[mediaWithPrefixSlug]: {
|
||||
media: true,
|
||||
'media-with-prefix': {
|
||||
prefix,
|
||||
},
|
||||
},
|
||||
@@ -137,8 +137,8 @@ export default buildConfig({
|
||||
plugins: [
|
||||
azureStorage({
|
||||
collections: {
|
||||
[mediaSlug]: true,
|
||||
[mediaWithPrefixSlug]: {
|
||||
media: true,
|
||||
'media-with-prefix': {
|
||||
prefix,
|
||||
},
|
||||
},
|
||||
@@ -186,8 +186,8 @@ export default buildConfig({
|
||||
plugins: [
|
||||
gcsStorage({
|
||||
collections: {
|
||||
[mediaSlug]: true,
|
||||
[mediaWithPrefixSlug]: {
|
||||
media: true,
|
||||
'media-with-prefix': {
|
||||
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 an API key from Uploadthing and set it as `apiKey` in the `options` object.
|
||||
- Get a token from Uploadthing and set it as `token` in the `options` object.
|
||||
- `acl` is optional and defaults to `public-read`.
|
||||
|
||||
```ts
|
||||
@@ -233,10 +233,10 @@ export default buildConfig({
|
||||
plugins: [
|
||||
uploadthingStorage({
|
||||
collections: {
|
||||
[mediaSlug]: true,
|
||||
media: true,
|
||||
},
|
||||
options: {
|
||||
apiKey: process.env.UPLOADTHING_SECRET,
|
||||
token: process.env.UPLOADTHING_TOKEN,
|
||||
acl: 'public-read',
|
||||
},
|
||||
}),
|
||||
@@ -248,7 +248,7 @@ export default buildConfig({
|
||||
|
||||
| Option | Description | Default |
|
||||
| ---------------- | ----------------------------------------------- | ------------- |
|
||||
| `apiKey` | API key from Uploadthing. Required. | |
|
||||
| `token` | Token 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` |
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
MONGODB_URI=mongodb://127.0.0.1/payload-example-email
|
||||
PAYLOAD_SECRET=
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-example-email
|
||||
NODE_ENV=development
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:8000
|
||||
PAYLOAD_SECRET=PAYLOAD_EMAIL_EXAMPLE_SECRET_KEY
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['@payloadcms'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
},
|
||||
}
|
||||
|
||||
24
examples/email/.swcrc
Normal file
24
examples/email/.swcrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
@@ -7,30 +7,31 @@ This example demonstrates how to integrate email functionality into Payload.
|
||||
To spin up this example locally, follow these steps:
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
2. `cp .env.example .env` to copy the example environment variables
|
||||
3. `pnpm install && pnpm dev` to install dependencies and start the dev server
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:8000/admin` to access the admin panel
|
||||
5. open `http://localhost:3000/admin` to access the admin panel
|
||||
6. Create your first user
|
||||
|
||||
## How it works
|
||||
|
||||
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({})`.
|
||||
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.
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
Now we can start sending email!
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
Let's not forget our authentication 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.
|
||||
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.
|
||||
|
||||
Speaking of customization...
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -40,10 +41,10 @@ To spin up this example locally, follow the [Quick Start](#quick-start).
|
||||
|
||||
## Production
|
||||
|
||||
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
|
||||
To run Payload in production, you need to build and start the Admin panel. To do so, follow these steps:
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### Deployment
|
||||
|
||||
|
||||
5
examples/email/next-env.d.ts
vendored
Normal file
5
examples/email/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <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.
|
||||
8
examples/email/next.config.mjs
Normal file
8
examples/email/next.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Your Next.js config here
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig)
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts -- -I",
|
||||
"stdin": false
|
||||
}
|
||||
@@ -1,35 +1,57 @@
|
||||
{
|
||||
"name": "payload-example-email",
|
||||
"description": "Payload Email integration example.",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"description": "Payload Email integration example.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"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"
|
||||
"_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"
|
||||
},
|
||||
"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",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest",
|
||||
"handlebars": "^4.7.7",
|
||||
"inline-css": "^4.0.2"
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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"
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
||||
6932
examples/email/pnpm-lock.yaml
generated
Normal file
6932
examples/email/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
/* 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
|
||||
@@ -0,0 +1,25 @@
|
||||
/* 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
|
||||
1
examples/email/src/app/(payload)/admin/importMap.js
Normal file
1
examples/email/src/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1 @@
|
||||
export const importMap = {}
|
||||
10
examples/email/src/app/(payload)/api/[...slug]/route.ts
Normal file
10
examples/email/src/app/(payload)/api/[...slug]/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* 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)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* 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)
|
||||
8
examples/email/src/app/(payload)/api/graphql/route.ts
Normal file
8
examples/email/src/app/(payload)/api/graphql/route.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/* 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)
|
||||
32
examples/email/src/app/(payload)/layout.tsx
Normal file
32
examples/email/src/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
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
|
||||
@@ -1,28 +1,12 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import generateEmailHTML from '../email/generateEmailHTML'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
const Newsletter: CollectionConfig = {
|
||||
import { generateEmailHTML } from '../email/generateEmailHTML'
|
||||
|
||||
export 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',
|
||||
@@ -34,6 +18,25 @@ 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
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import generateForgotPasswordEmail from '../email/generateForgotPasswordEmail'
|
||||
import generateVerificationEmail from '../email/generateVerificationEmail'
|
||||
import { generateForgotPasswordEmail } from '../email/generateForgotPasswordEmail'
|
||||
import { generateVerificationEmail } from '../email/generateVerificationEmail'
|
||||
|
||||
const Users: CollectionConfig = {
|
||||
export 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,5 +25,3 @@ const Users: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default Users
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import ejs from 'ejs'
|
||||
import fs from 'fs'
|
||||
import Handlebars from 'handlebars'
|
||||
import inlineCSS from 'inline-css'
|
||||
import juice from 'juice'
|
||||
import path from 'path'
|
||||
|
||||
const template = fs.readFileSync
|
||||
? fs.readFileSync(path.join(__dirname, './template.html'), 'utf8')
|
||||
: ''
|
||||
export const generateEmailHTML = async (data: any): Promise<string> => {
|
||||
const templatePath = path.join(process.cwd(), 'src/email/template.ejs')
|
||||
const templateContent = fs.readFileSync(templatePath, 'utf8')
|
||||
|
||||
// Compile the template
|
||||
const getHTML = Handlebars.compile(template)
|
||||
// Compile and render the template with EJS
|
||||
const preInlinedCSS = ejs.render(templateContent, { ...data, cta: data.cta || {} })
|
||||
|
||||
const generateEmailHTML = async (data): Promise<string> => {
|
||||
const preInlinedCSS = getHTML(data)
|
||||
// Inline CSS
|
||||
const html = juice(preInlinedCSS)
|
||||
|
||||
const html = await inlineCSS(preInlinedCSS, {
|
||||
url: ' ',
|
||||
removeStyleTags: false,
|
||||
})
|
||||
|
||||
return html
|
||||
return Promise.resolve(html)
|
||||
}
|
||||
|
||||
export default generateEmailHTML
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import generateEmailHTML from './generateEmailHTML'
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
const generateForgotPasswordEmail = async ({ token }): Promise<string> =>
|
||||
generateEmailHTML({
|
||||
headline: 'Locked out?',
|
||||
import { generateEmailHTML } from './generateEmailHTML'
|
||||
|
||||
type ForgotPasswordEmailArgs =
|
||||
| {
|
||||
req?: PayloadRequest
|
||||
token?: string
|
||||
user?: any
|
||||
}
|
||||
| undefined
|
||||
|
||||
export const generateForgotPasswordEmail = async (
|
||||
args: ForgotPasswordEmailArgs,
|
||||
): Promise<string> => {
|
||||
return generateEmailHTML({
|
||||
content: '<p>Let's get you back in.</p>',
|
||||
cta: {
|
||||
buttonLabel: 'Reset your password',
|
||||
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/reset-password?token=${token}`,
|
||||
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/reset-password?token=${args?.token}`,
|
||||
},
|
||||
headline: 'Locked out?',
|
||||
})
|
||||
|
||||
export default generateForgotPasswordEmail
|
||||
}
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import generateEmailHTML from './generateEmailHTML'
|
||||
import { generateEmailHTML } from './generateEmailHTML'
|
||||
|
||||
const generateVerificationEmail = async (args): Promise<string> => {
|
||||
const { user, token } = args
|
||||
type User = {
|
||||
email: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
type GenerateVerificationEmailArgs = {
|
||||
token: string
|
||||
user: User
|
||||
}
|
||||
|
||||
export const generateVerificationEmail = async (
|
||||
args: GenerateVerificationEmailArgs,
|
||||
): Promise<string> => {
|
||||
const { token, user } = 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
|
||||
|
||||
327
examples/email/src/email/template.ejs
Normal file
327
examples/email/src/email/template.ejs
Normal file
@@ -0,0 +1,327 @@
|
||||
<!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"> </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"> </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>
|
||||
@@ -1,345 +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"
|
||||
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"> </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"> </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>
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
export default {}
|
||||
@@ -1,4 +1,5 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
@@ -6,30 +7,213 @@
|
||||
*/
|
||||
|
||||
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;
|
||||
name?: string | null;
|
||||
email: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
_verified?: boolean;
|
||||
_verificationToken?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
name?: string | null;
|
||||
updatedAt: string;
|
||||
password?: 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;
|
||||
}
|
||||
/**
|
||||
* 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 {}
|
||||
}
|
||||
@@ -1,42 +1,37 @@
|
||||
import dotenv from 'dotenv'
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import Users from './collections/Users'
|
||||
import Newsletter from './collections/Newsletter'
|
||||
import { Newsletter } from './collections/Newsletter'
|
||||
import { Users } from './collections/Users'
|
||||
|
||||
dotenv.config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
})
|
||||
|
||||
const mockModulePath = path.resolve(__dirname, './emptyModule.js')
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
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,
|
||||
),
|
||||
},
|
||||
}),
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
user: Users.slug,
|
||||
},
|
||||
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'),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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()
|
||||
@@ -1,24 +1,48 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"baseUrl": ".",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"payload/generated-types": ["./src/payload-types.ts"],
|
||||
"node_modules/*": ["./node_modules/*"]
|
||||
}
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@payload-config": [
|
||||
"src/payload.config.ts"
|
||||
],
|
||||
"@payload-types": [
|
||||
"src/payload-types.ts"
|
||||
]
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "build"],
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"src/mocks/emptyObject.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -71,7 +71,7 @@ const vercelBlobStorageReplacement: StorageAdapterReplacement = {
|
||||
configReplacement: [
|
||||
' vercelBlobStorage({',
|
||||
' collections: {',
|
||||
' [Media.slug]: true,',
|
||||
' media: true,',
|
||||
' },',
|
||||
" token: process.env.BLOB_READ_WRITE_TOKEN || '',",
|
||||
' }),',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -23,13 +23,17 @@
|
||||
"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",
|
||||
"types": "./src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"mock.js",
|
||||
"predefinedMigrations"
|
||||
],
|
||||
"scripts": {
|
||||
@@ -42,18 +46,17 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"dependencies": {
|
||||
"bson-objectid": "2.0.4",
|
||||
"http-status": "1.6.2",
|
||||
"mongoose": "6.12.3",
|
||||
"mongoose-aggregate-paginate-v2": "1.0.6",
|
||||
"mongoose-paginate-v2": "1.7.22",
|
||||
"mongoose": "8.8.1",
|
||||
"mongoose-aggregate-paginate-v2": "1.1.2",
|
||||
"mongoose-paginate-v2": "1.8.5",
|
||||
"prompts": "2.4.2",
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/mongoose-aggregate-paginate-v2": "1.0.6",
|
||||
"mongodb": "4.17.1",
|
||||
"@types/mongoose-aggregate-paginate-v2": "1.0.12",
|
||||
"mongodb": "6.10.0",
|
||||
"mongodb-memory-server": "^9",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
@@ -66,6 +69,11 @@
|
||||
"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",
|
||||
|
||||
@@ -60,14 +60,7 @@ export const connect: Connect = async function connect(
|
||||
if (this.ensureIndexes) {
|
||||
await Promise.all(
|
||||
this.payload.config.collections.map(async (coll) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
this.collections[coll.slug]?.ensureIndexes(function (err) {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
await this.collections[coll.slug]?.ensureIndexes()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { CountOptions } from 'mongodb'
|
||||
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: QueryOptions = await withSession(this, req)
|
||||
const options: CountOptions = await withSession(this, req)
|
||||
|
||||
let hasNearConstraint = false
|
||||
|
||||
@@ -40,7 +40,12 @@ export const count: Count = async function count(
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Model.countDocuments(query, options)
|
||||
let result: number
|
||||
if (useEstimatedCount) {
|
||||
result = await Model.estimatedDocumentCount({ session: options.session })
|
||||
} else {
|
||||
result = await Model.countDocuments(query, options)
|
||||
}
|
||||
|
||||
return {
|
||||
totalDocs: result,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { CountOptions } from 'mongodb'
|
||||
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: QueryOptions = await withSession(this, req)
|
||||
const options: CountOptions = await withSession(this, req)
|
||||
|
||||
let hasNearConstraint = false
|
||||
|
||||
@@ -40,7 +40,12 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Model.countDocuments(query, options)
|
||||
let result: number
|
||||
if (useEstimatedCount) {
|
||||
result = await Model.estimatedDocumentCount({ session: options.session })
|
||||
} else {
|
||||
result = await Model.countDocuments(query, options)
|
||||
}
|
||||
|
||||
return {
|
||||
totalDocs: result,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { CountOptions } from 'mongodb'
|
||||
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: QueryOptions = await withSession(this, req)
|
||||
const options: CountOptions = await withSession(this, req)
|
||||
|
||||
let hasNearConstraint = false
|
||||
|
||||
@@ -40,7 +40,12 @@ export const countVersions: CountVersions = async function countVersions(
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Model.countDocuments(query, options)
|
||||
let result: number
|
||||
if (useEstimatedCount) {
|
||||
result = await Model.estimatedDocumentCount({ session: options.session })
|
||||
} else {
|
||||
result = await Model.countDocuments(query, options)
|
||||
}
|
||||
|
||||
return {
|
||||
totalDocs: result,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import mongoose from 'mongoose'
|
||||
import { Types } from 'mongoose'
|
||||
import {
|
||||
buildVersionCollectionFields,
|
||||
type CreateVersion,
|
||||
@@ -57,7 +57,7 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
},
|
||||
],
|
||||
}
|
||||
if (data.parent instanceof mongoose.Types.ObjectId) {
|
||||
if (data.parent instanceof Types.ObjectId) {
|
||||
parentQuery.$or.push({
|
||||
parent: {
|
||||
$eq: data.parent.toString(),
|
||||
|
||||
2
packages/db-mongodb/src/exports/migration-utils.ts
Normal file
2
packages/db-mongodb/src/exports/migration-utils.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { migrateRelationshipsV2_V3 } from '../predefinedMigrations/migrateRelationshipsV2_V3.js'
|
||||
export { migrateVersionsV1_V2 } from '../predefinedMigrations/migrateVersionsV1_V2.js'
|
||||
@@ -58,7 +58,6 @@ 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,
|
||||
|
||||
@@ -64,7 +64,6 @@ 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,
|
||||
|
||||
@@ -60,7 +60,6 @@ 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,
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
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, UpdateOneArgs } from 'payload'
|
||||
import type {
|
||||
BaseDatabaseAdapter,
|
||||
DatabaseAdapterObj,
|
||||
Payload,
|
||||
TypeWithID,
|
||||
TypeWithVersion,
|
||||
UpdateGlobalArgs,
|
||||
UpdateGlobalVersionArgs,
|
||||
UpdateOneArgs,
|
||||
UpdateVersionArgs,
|
||||
} from 'payload'
|
||||
|
||||
import fs from 'fs'
|
||||
import mongoose from 'mongoose'
|
||||
@@ -135,7 +145,16 @@ 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
|
||||
}
|
||||
|
||||
@@ -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.config)
|
||||
const schema = buildCollectionSchema(collection, this.payload)
|
||||
|
||||
if (collection.versions) {
|
||||
const versionModelName = getDBName({ config: collection, versions: true })
|
||||
|
||||
const versionCollectionFields = buildVersionCollectionFields(this.payload.config, collection)
|
||||
|
||||
const versionSchema = buildSchema(this.payload.config, versionCollectionFields, {
|
||||
const versionSchema = buildSchema(this.payload, 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.config)
|
||||
this.globals = buildGlobalModel(this.payload)
|
||||
|
||||
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.config, versionGlobalFields, {
|
||||
const versionSchema = buildSchema(this.payload, versionGlobalFields, {
|
||||
disableUnique: true,
|
||||
draftsEnabled: true,
|
||||
indexSortableFields: this.payload.config.indexSortableFields,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PaginateOptions, Schema } from 'mongoose'
|
||||
import type { SanitizedCollectionConfig, SanitizedConfig } from 'payload'
|
||||
import type { Payload, SanitizedCollectionConfig } 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,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
schemaOptions = {},
|
||||
): Schema => {
|
||||
const schema = buildSchema(config, collection.fields, {
|
||||
const schema = buildSchema(payload, collection.fields, {
|
||||
draftsEnabled: Boolean(typeof collection?.versions === 'object' && collection.versions.drafts),
|
||||
indexSortableFields: config.indexSortableFields,
|
||||
indexSortableFields: payload.config.indexSortableFields,
|
||||
options: {
|
||||
minimize: false,
|
||||
timestamps: collection.timestamps !== false,
|
||||
@@ -34,7 +34,7 @@ export const buildCollectionSchema = (
|
||||
schema.index(indexDefinition, { unique: true })
|
||||
}
|
||||
|
||||
if (config.indexSortableFields && collection.timestamps !== false) {
|
||||
if (payload.config.indexSortableFields && collection.timestamps !== false) {
|
||||
schema.index({ updatedAt: 1 })
|
||||
schema.index({ createdAt: 1 })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
import type { Payload } 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 = (config: SanitizedConfig): GlobalModel | null => {
|
||||
if (config.globals && config.globals.length > 0) {
|
||||
export const buildGlobalModel = (payload: Payload): GlobalModel | null => {
|
||||
if (payload.config.globals && payload.config.globals.length > 0) {
|
||||
const globalsSchema = new mongoose.Schema(
|
||||
{},
|
||||
{ discriminatorKey: 'globalType', minimize: false, timestamps: true },
|
||||
@@ -18,8 +18,8 @@ export const buildGlobalModel = (config: SanitizedConfig): GlobalModel | null =>
|
||||
|
||||
const Globals = mongoose.model('globals', globalsSchema, 'globals') as unknown as GlobalModel
|
||||
|
||||
Object.values(config.globals).forEach((globalConfig) => {
|
||||
const globalSchema = buildSchema(config, globalConfig.fields, {
|
||||
Object.values(payload.config.globals).forEach((globalConfig) => {
|
||||
const globalSchema = buildSchema(payload, globalConfig.fields, {
|
||||
options: {
|
||||
minimize: false,
|
||||
},
|
||||
|
||||
@@ -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: SanitizedConfig,
|
||||
config: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
) => void
|
||||
|
||||
@@ -113,7 +113,7 @@ const localizeSchema = (
|
||||
}
|
||||
|
||||
export const buildSchema = (
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
configFields: Field[],
|
||||
buildSchemaOptions: BuildSchemaOptions = {},
|
||||
): Schema => {
|
||||
@@ -145,7 +145,7 @@ export const buildSchema = (
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type]
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(field, schema, config, buildSchemaOptions)
|
||||
addFieldSchema(field, schema, payload, buildSchemaOptions)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -157,13 +157,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
array: (
|
||||
field: ArrayField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
) => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: [
|
||||
buildSchema(config, field.fields, {
|
||||
buildSchema(payload, 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, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
blocks: (
|
||||
field: BlocksField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const fieldSchema = {
|
||||
@@ -191,7 +191,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, fieldSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, fieldSchema, payload.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, config, buildSchemaOptions)
|
||||
addFieldSchema(blockField, blockSchema, payload, buildSchemaOptions)
|
||||
}
|
||||
})
|
||||
|
||||
if (field.localized && config.localization) {
|
||||
config.localization.localeCodes.forEach((localeCode) => {
|
||||
if (field.localized && payload.config.localization) {
|
||||
payload.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,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
code: (
|
||||
field: CodeField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
collapsible: (
|
||||
field: CollapsibleField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
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, config, buildSchemaOptions)
|
||||
addFieldSchema(subField, schema, payload, buildSchemaOptions)
|
||||
}
|
||||
})
|
||||
},
|
||||
date: (
|
||||
field: DateField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
email: (
|
||||
field: EmailField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
group: (
|
||||
field: GroupField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions)
|
||||
@@ -297,7 +297,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
|
||||
const baseSchema = {
|
||||
...formattedBaseSchema,
|
||||
type: buildSchema(config, field.fields, {
|
||||
type: buildSchema(payload, 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, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
json: (
|
||||
field: JSONField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
@@ -325,13 +325,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
number: (
|
||||
field: NumberField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
@@ -340,13 +340,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
point: (
|
||||
field: PointField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema: SchemaTypeOptions<unknown> = {
|
||||
@@ -368,7 +368,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.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 && config.localization) {
|
||||
config.localization.locales.forEach((locale) => {
|
||||
if (field.localized && payload.config.localization) {
|
||||
payload.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,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
@@ -404,21 +404,23 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
relationship: (
|
||||
field: RelationshipField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
) => {
|
||||
const hasManyRelations = Array.isArray(field.relationTo)
|
||||
let schemaToReturn: { [key: string]: any } = {}
|
||||
|
||||
if (field.localized && config.localization) {
|
||||
const valueType = getRelationshipValueType(field, payload)
|
||||
|
||||
if (field.localized && payload.config.localization) {
|
||||
schemaToReturn = {
|
||||
type: config.localization.localeCodes.reduce((locales, locale) => {
|
||||
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
|
||||
let localeSchema: { [key: string]: any } = {}
|
||||
|
||||
if (hasManyRelations) {
|
||||
@@ -428,14 +430,14 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
value: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
type: valueType,
|
||||
refPath: `${field.name}.${locale}.relationTo`,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
type: valueType,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
}
|
||||
@@ -456,7 +458,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
value: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
type: valueType,
|
||||
refPath: `${field.name}.relationTo`,
|
||||
},
|
||||
}
|
||||
@@ -470,7 +472,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
} else {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
type: valueType,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
|
||||
@@ -489,7 +491,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
richText: (
|
||||
field: RichTextField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
@@ -498,13 +500,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
row: (
|
||||
field: RowField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
field.fields.forEach((subField: Field) => {
|
||||
@@ -515,14 +517,14 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(subField, schema, config, buildSchemaOptions)
|
||||
addFieldSchema(subField, schema, payload, buildSchemaOptions)
|
||||
}
|
||||
})
|
||||
},
|
||||
select: (
|
||||
field: SelectField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
@@ -544,14 +546,14 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
field.hasMany ? [baseSchema] : baseSchema,
|
||||
config.localization,
|
||||
payload.config.localization,
|
||||
),
|
||||
})
|
||||
},
|
||||
tabs: (
|
||||
field: TabsField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
field.tabs.forEach((tab) => {
|
||||
@@ -560,7 +562,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
return
|
||||
}
|
||||
const baseSchema = {
|
||||
type: buildSchema(config, tab.fields, {
|
||||
type: buildSchema(payload, tab.fields, {
|
||||
disableUnique: buildSchemaOptions.disableUnique,
|
||||
draftsEnabled: buildSchemaOptions.draftsEnabled,
|
||||
options: {
|
||||
@@ -572,7 +574,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[tab.name]: localizeSchema(tab, baseSchema, config.localization),
|
||||
[tab.name]: localizeSchema(tab, baseSchema, payload.config.localization),
|
||||
})
|
||||
} else {
|
||||
tab.fields.forEach((subField: Field) => {
|
||||
@@ -582,7 +584,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(subField, schema, config, buildSchemaOptions)
|
||||
addFieldSchema(subField, schema, payload, buildSchemaOptions)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -591,7 +593,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
text: (
|
||||
field: TextField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
@@ -600,33 +602,35 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
textarea: (
|
||||
field: TextareaField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
|
||||
})
|
||||
},
|
||||
upload: (
|
||||
field: UploadField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
payload: Payload,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const hasManyRelations = Array.isArray(field.relationTo)
|
||||
let schemaToReturn: { [key: string]: any } = {}
|
||||
|
||||
if (field.localized && config.localization) {
|
||||
const valueType = getRelationshipValueType(field, payload)
|
||||
|
||||
if (field.localized && payload.config.localization) {
|
||||
schemaToReturn = {
|
||||
type: config.localization.localeCodes.reduce((locales, locale) => {
|
||||
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
|
||||
let localeSchema: { [key: string]: any } = {}
|
||||
|
||||
if (hasManyRelations) {
|
||||
@@ -636,14 +640,14 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
value: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
type: valueType,
|
||||
refPath: `${field.name}.${locale}.relationTo`,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
type: valueType,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
}
|
||||
@@ -664,7 +668,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
value: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
type: valueType,
|
||||
refPath: `${field.name}.relationTo`,
|
||||
},
|
||||
}
|
||||
@@ -678,7 +682,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
} else {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
type: valueType,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
|
||||
@@ -695,3 +699,30 @@ 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
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}"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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 })
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const imports = `import { migrateRelationshipsV2_V3 } from '@payloadcms/db-mongodb/migration-utils'`
|
||||
const upSQL = ` await migrateRelationshipsV2_V3({
|
||||
batchSize: 100,
|
||||
req,
|
||||
})
|
||||
`
|
||||
export { imports, upSQL }
|
||||
@@ -1,96 +0,0 @@
|
||||
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.\`)
|
||||
}
|
||||
}),
|
||||
)
|
||||
`
|
||||
@@ -0,0 +1,6 @@
|
||||
const imports = `import { migrateVersionsV1_V2 } from '@payloadcms/db-mongodb/migration-utils'`
|
||||
const upSQL = ` await migrateVersionsV1_V2({
|
||||
req,
|
||||
})
|
||||
`
|
||||
export { imports, upSQL }
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Field, Operator, PathToQuery, Payload } from 'payload'
|
||||
|
||||
import ObjectIdImport from 'bson-objectid'
|
||||
import mongoose from 'mongoose'
|
||||
import { Types } from 'mongoose'
|
||||
import { getLocalizedPaths } from 'payload'
|
||||
import { validOperators } from 'payload/shared'
|
||||
|
||||
@@ -10,9 +9,6 @@ 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
|
||||
@@ -87,13 +83,13 @@ export async function buildSearchParam({
|
||||
}
|
||||
|
||||
const [{ field, path }] = paths
|
||||
|
||||
if (path) {
|
||||
const sanitizedQueryValue = sanitizeQueryValue({
|
||||
field,
|
||||
hasCustomID,
|
||||
operator,
|
||||
path,
|
||||
payload,
|
||||
val,
|
||||
})
|
||||
|
||||
@@ -145,7 +141,7 @@ export async function buildSearchParam({
|
||||
const stringID = doc._id.toString()
|
||||
$in.push(stringID)
|
||||
|
||||
if (mongoose.Types.ObjectId.isValid(stringID)) {
|
||||
if (Types.ObjectId.isValid(stringID)) {
|
||||
$in.push(doc._id)
|
||||
}
|
||||
})
|
||||
@@ -207,9 +203,9 @@ export async function buildSearchParam({
|
||||
}
|
||||
|
||||
if (typeof formattedValue === 'string') {
|
||||
if (mongoose.Types.ObjectId.isValid(formattedValue)) {
|
||||
if (Types.ObjectId.isValid(formattedValue)) {
|
||||
result.value[multiIDCondition].push({
|
||||
[path]: { [operatorKey]: ObjectId(formattedValue) },
|
||||
[path]: { [operatorKey]: new Types.ObjectId(formattedValue) },
|
||||
})
|
||||
} else {
|
||||
;(Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]).forEach(
|
||||
|
||||
@@ -71,7 +71,10 @@ export async function parseParams({
|
||||
[searchParam.path]: searchParam.value,
|
||||
}
|
||||
} else if (typeof searchParam?.value === 'object') {
|
||||
result = deepMergeWithCombinedArrays(result, searchParam.value)
|
||||
result = deepMergeWithCombinedArrays(result, searchParam.value, {
|
||||
// dont clone Types.ObjectIDs
|
||||
clone: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import type { Field, TabAsField } from 'payload'
|
||||
import type { Block, Field, Payload, RelationshipField, TabAsField } from 'payload'
|
||||
|
||||
import ObjectIdImport from 'bson-objectid'
|
||||
import mongoose from 'mongoose'
|
||||
import { createArrayFromCommaDelineated } from 'payload'
|
||||
import { Types } from 'mongoose'
|
||||
import { createArrayFromCommaDelineated, flattenTopLevelFields } from 'payload'
|
||||
|
||||
type SanitizeQueryValueArgs = {
|
||||
field: Field | TabAsField
|
||||
hasCustomID: boolean
|
||||
operator: string
|
||||
path: string
|
||||
payload: Payload
|
||||
val: any
|
||||
}
|
||||
|
||||
const buildExistsQuery = (formattedValue, path) => {
|
||||
const buildExistsQuery = (formattedValue, path, treatEmptyString = true) => {
|
||||
if (formattedValue) {
|
||||
return {
|
||||
rawQuery: {
|
||||
$and: [
|
||||
{ [path]: { $exists: true } },
|
||||
{ [path]: { $ne: null } },
|
||||
{ [path]: { $ne: '' } }, // Exclude null and empty string
|
||||
...(treatEmptyString ? [{ [path]: { $ne: '' } }] : []), // Treat empty string as null / undefined
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -29,20 +29,56 @@ const buildExistsQuery = (formattedValue, path) => {
|
||||
$or: [
|
||||
{ [path]: { $exists: false } },
|
||||
{ [path]: { $eq: null } },
|
||||
{ [path]: { $eq: '' } }, // Treat empty string as null / undefined
|
||||
...(treatEmptyString ? [{ [path]: { $eq: '' } }] : []), // Treat empty string as null / undefined
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ObjectId = (ObjectIdImport.default ||
|
||||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
|
||||
// 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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sanitizeQueryValue = ({
|
||||
field,
|
||||
hasCustomID,
|
||||
operator,
|
||||
path,
|
||||
payload,
|
||||
val,
|
||||
}: SanitizeQueryValueArgs): {
|
||||
operator?: string
|
||||
@@ -52,21 +88,31 @@ 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 = mongoose.Types.ObjectId.isValid(val)
|
||||
const isValid = Types.ObjectId.isValid(val)
|
||||
|
||||
if (!isValid) {
|
||||
return { operator: formattedOperator, val: undefined }
|
||||
} else {
|
||||
if (['in', 'not_in'].includes(operator)) {
|
||||
formattedValue = createArrayFromCommaDelineated(formattedValue).map((id) =>
|
||||
ObjectId(id),
|
||||
formattedValue = createArrayFromCommaDelineated(formattedValue).map(
|
||||
(id) => new Types.ObjectId(id),
|
||||
)
|
||||
} else {
|
||||
formattedValue = ObjectId(val)
|
||||
formattedValue = new Types.ObjectId(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,21 +130,22 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
|
||||
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
|
||||
const newValues = [inVal]
|
||||
if (!hasCustomID) {
|
||||
if (mongoose.Types.ObjectId.isValid(inVal)) {
|
||||
newValues.push(ObjectId(inVal))
|
||||
if (Types.ObjectId.isValid(inVal)) {
|
||||
formattedValues.push(new Types.ObjectId(inVal))
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'number') {
|
||||
const parsedNumber = parseFloat(inVal)
|
||||
if (!Number.isNaN(parsedNumber)) {
|
||||
newValues.push(parsedNumber)
|
||||
formattedValues.push(parsedNumber)
|
||||
}
|
||||
} else {
|
||||
formattedValues.push(inVal)
|
||||
}
|
||||
|
||||
return [...formattedValues, ...newValues]
|
||||
return formattedValues
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
@@ -154,10 +201,10 @@ export const sanitizeQueryValue = ({
|
||||
formattedValue.relationTo
|
||||
) {
|
||||
const { value } = formattedValue
|
||||
const isValid = mongoose.Types.ObjectId.isValid(value)
|
||||
const isValid = Types.ObjectId.isValid(value)
|
||||
|
||||
if (isValid) {
|
||||
formattedValue.value = ObjectId(value)
|
||||
formattedValue.value = new Types.ObjectId(value)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -170,25 +217,88 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
}
|
||||
|
||||
const relationTo = (field as RelationshipField).relationTo
|
||||
|
||||
if (['in', 'not_in'].includes(operator) && Array.isArray(formattedValue)) {
|
||||
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
|
||||
const newValues = [inVal]
|
||||
if (mongoose.Types.ObjectId.isValid(inVal)) {
|
||||
newValues.push(ObjectId(inVal))
|
||||
if (!inVal) {
|
||||
return formattedValues
|
||||
}
|
||||
|
||||
const parsedNumber = parseFloat(inVal)
|
||||
if (!Number.isNaN(parsedNumber)) {
|
||||
newValues.push(parsedNumber)
|
||||
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
|
||||
}
|
||||
|
||||
return [...formattedValues, ...newValues]
|
||||
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
|
||||
}, [])
|
||||
}
|
||||
|
||||
if (operator === 'contains' && typeof formattedValue === 'string') {
|
||||
if (mongoose.Types.ObjectId.isValid(formattedValue)) {
|
||||
formattedValue = ObjectId(formattedValue)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,7 +342,7 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
|
||||
if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) {
|
||||
if (operator === 'contains' && !mongoose.Types.ObjectId.isValid(formattedValue)) {
|
||||
if (operator === 'contains' && !Types.ObjectId.isValid(formattedValue)) {
|
||||
formattedValue = {
|
||||
$options: 'i',
|
||||
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
@@ -242,7 +352,12 @@ export const sanitizeQueryValue = ({
|
||||
if (operator === 'exists') {
|
||||
formattedValue = formattedValue === 'true' || formattedValue === true
|
||||
|
||||
return buildExistsQuery(formattedValue, path)
|
||||
// _id can't be empty string, will error Cast to ObjectId failed for value ""
|
||||
return buildExistsQuery(
|
||||
formattedValue,
|
||||
path,
|
||||
!['relationship', 'upload'].includes(field.type),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ 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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { PayloadRequest, UpdateGlobal } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
@@ -9,12 +10,13 @@ import { withSession } from './withSession.js'
|
||||
|
||||
export const updateGlobal: UpdateGlobal = async function updateGlobal(
|
||||
this: MongooseAdapter,
|
||||
{ slug, data, req = {} as PayloadRequest, select },
|
||||
{ slug, data, options: optionsArgs = {}, req = {} as PayloadRequest, select },
|
||||
) {
|
||||
const Model = this.globals
|
||||
const fields = this.payload.config.globals.find((global) => global.slug === slug).fields
|
||||
|
||||
const options = {
|
||||
const options: QueryOptions = {
|
||||
...optionsArgs,
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
new: true,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
|
||||
import {
|
||||
buildVersionGlobalFields,
|
||||
type PayloadRequest,
|
||||
@@ -17,6 +19,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
id,
|
||||
global: globalSlug,
|
||||
locale,
|
||||
options: optionsArgs = {},
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
versionData,
|
||||
@@ -30,7 +33,8 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
this.payload.config.globals.find((global) => global.slug === globalSlug),
|
||||
)
|
||||
|
||||
const options = {
|
||||
const options: QueryOptions = {
|
||||
...optionsArgs,
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
new: true,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
|
||||
import { buildVersionCollectionFields, type PayloadRequest, type UpdateVersion } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
@@ -8,7 +10,16 @@ import { withSession } from './withSession.js'
|
||||
|
||||
export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
this: MongooseAdapter,
|
||||
{ id, collection, locale, req = {} as PayloadRequest, select, versionData, where },
|
||||
{
|
||||
id,
|
||||
collection,
|
||||
locale,
|
||||
options: optionsArgs = {},
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
versionData,
|
||||
where,
|
||||
},
|
||||
) {
|
||||
const VersionModel = this.versions[collection]
|
||||
const whereToUse = where || { id: { equals: id } }
|
||||
@@ -17,7 +28,8 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
this.payload.collections[collection].config,
|
||||
)
|
||||
|
||||
const options = {
|
||||
const options: QueryOptions = {
|
||||
...optionsArgs,
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
new: true,
|
||||
|
||||
@@ -106,7 +106,6 @@ const traverseFields = ({
|
||||
switch (field.type) {
|
||||
case 'array':
|
||||
case 'group':
|
||||
|
||||
case 'tab': {
|
||||
let fieldSelect: SelectType
|
||||
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
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())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
|
||||
|
||||
import mongoose from 'mongoose'
|
||||
import { Types } from 'mongoose'
|
||||
import { APIError, traverseFields } from 'payload'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
|
||||
@@ -25,14 +25,14 @@ const convertValue = ({
|
||||
}: {
|
||||
relatedCollection: CollectionConfig
|
||||
value: number | string
|
||||
}): mongoose.Types.ObjectId | number | string => {
|
||||
}): number | string | Types.ObjectId => {
|
||||
const customIDField = relatedCollection.fields.find(
|
||||
(field) => fieldAffectsData(field) && field.name === 'id',
|
||||
)
|
||||
|
||||
if (!customIDField) {
|
||||
try {
|
||||
return new mongoose.Types.ObjectId(value)
|
||||
return new 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, ref: data })
|
||||
traverseFields({ callback: sanitize, fields, fillEmpty: false, ref: data })
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -50,12 +50,17 @@ import {
|
||||
requireDrizzleKit,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
|
||||
import path from 'path'
|
||||
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { Args, PostgresAdapter } from './types.js'
|
||||
|
||||
import { connect } from './connect.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter> {
|
||||
const postgresIDType = args.idType || 'serial'
|
||||
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
|
||||
@@ -88,6 +93,9 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
createDatabase,
|
||||
createExtensions,
|
||||
createMigration(args) {
|
||||
return createMigration.bind(this)({ ...args, dirname })
|
||||
},
|
||||
defaultDrizzleSnapshot,
|
||||
disableCreateDatabase: args.disableCreateDatabase ?? false,
|
||||
drizzle: undefined,
|
||||
@@ -132,7 +140,6 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
createVersion,
|
||||
defaultIDType: payloadIDType,
|
||||
deleteMany,
|
||||
|
||||
@@ -58,8 +58,8 @@ export const traverseFields = (args: Args) => {
|
||||
})
|
||||
})
|
||||
}
|
||||
case 'collapsible':
|
||||
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
return traverseFields({
|
||||
...args,
|
||||
@@ -84,7 +84,6 @@ export const traverseFields = (args: Args) => {
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
|
||||
case 'upload': {
|
||||
if (typeof field.relationTo === 'string') {
|
||||
if (field.type === 'upload' || !field.hasMany) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { Connect } from 'payload'
|
||||
|
||||
import { createClient } from '@libsql/client'
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
SQLiteTableWithColumns,
|
||||
UniqueConstraintBuilder,
|
||||
} from 'drizzle-orm/sqlite-core'
|
||||
import type { Field, SanitizedJoins } from 'payload'
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { buildIndexName, createTableName } from '@payloadcms/drizzle'
|
||||
import { relations, sql } from 'drizzle-orm'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Relation } from 'drizzle-orm'
|
||||
import type { IndexBuilder, SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core'
|
||||
import type { Field, SanitizedJoins, TabAsField } from 'payload'
|
||||
import type { Field, TabAsField } from 'payload'
|
||||
|
||||
import {
|
||||
buildIndexName,
|
||||
@@ -472,16 +472,15 @@ export const traverseFields = ({
|
||||
targetTable[fieldName] = withDefault(integer(columnName, { mode: 'boolean' }), field)
|
||||
break
|
||||
}
|
||||
|
||||
case 'code':
|
||||
|
||||
case 'email':
|
||||
|
||||
case 'textarea': {
|
||||
targetTable[fieldName] = withDefault(text(columnName), field)
|
||||
break
|
||||
}
|
||||
case 'collapsible':
|
||||
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
const {
|
||||
@@ -654,7 +653,6 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
case 'json':
|
||||
|
||||
case 'richText': {
|
||||
targetTable[fieldName] = withDefault(text(columnName, { mode: 'json' }), field)
|
||||
break
|
||||
@@ -691,8 +689,8 @@ export const traverseFields = ({
|
||||
case 'point': {
|
||||
break
|
||||
}
|
||||
case 'radio':
|
||||
|
||||
case 'radio':
|
||||
case 'select': {
|
||||
const options = field.options.map((option) => {
|
||||
if (optionIsObject(option)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -50,12 +50,17 @@ import {
|
||||
requireDrizzleKit,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
|
||||
import path from 'path'
|
||||
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { Args, VercelPostgresAdapter } from './types.js'
|
||||
|
||||
import { connect } from './connect.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<VercelPostgresAdapter> {
|
||||
const postgresIDType = args.idType || 'serial'
|
||||
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
|
||||
@@ -133,7 +138,9 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
createMigration(args) {
|
||||
return createMigration.bind(this)({ ...args, dirname })
|
||||
},
|
||||
createVersion,
|
||||
defaultIDType: payloadIDType,
|
||||
deleteMany,
|
||||
|
||||
@@ -58,8 +58,8 @@ export const traverseFields = (args: Args) => {
|
||||
})
|
||||
})
|
||||
}
|
||||
case 'collapsible':
|
||||
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
return traverseFields({
|
||||
...args,
|
||||
@@ -84,7 +84,6 @@ export const traverseFields = (args: Args) => {
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
|
||||
case 'upload': {
|
||||
if (typeof field.relationTo === 'string') {
|
||||
if (field.type === 'upload' || !field.hasMany) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -2,10 +2,8 @@ import type { CreateMigration } from 'payload'
|
||||
|
||||
import fs from 'fs'
|
||||
import { createRequire } from 'module'
|
||||
import path from 'path'
|
||||
import { getPredefinedMigration, writeMigrationIndex } from 'payload'
|
||||
import prompts from 'prompts'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { BasePostgresAdapter } from './types.js'
|
||||
|
||||
@@ -16,10 +14,8 @@ const require = createRequire(import.meta.url)
|
||||
|
||||
export const createMigration: CreateMigration = async function createMigration(
|
||||
this: BasePostgresAdapter,
|
||||
{ file, forceAcceptWarning, migrationName, payload },
|
||||
{ dirname, file, forceAcceptWarning, migrationName, payload },
|
||||
) {
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
const dir = payload.db.migrationDir
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.129",
|
||||
"version": "3.0.0-beta.131",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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: Permissions
|
||||
permissions: SanitizedPermissions
|
||||
}> = (props) => {
|
||||
const { collectionConfig, globalConfig, i18n, payload, permissions } = props
|
||||
const { config } = payload
|
||||
|
||||
@@ -72,9 +72,8 @@ export const tabs: Record<
|
||||
condition: ({ collectionConfig, globalConfig, permissions }) =>
|
||||
Boolean(
|
||||
(collectionConfig?.versions &&
|
||||
permissions?.collections?.[collectionConfig?.slug]?.readVersions?.permission) ||
|
||||
(globalConfig?.versions &&
|
||||
permissions?.globals?.[globalConfig?.slug]?.readVersions?.permission),
|
||||
permissions?.collections?.[collectionConfig?.slug]?.readVersions) ||
|
||||
(globalConfig?.versions && permissions?.globals?.[globalConfig?.slug]?.readVersions),
|
||||
),
|
||||
href: '/versions',
|
||||
label: ({ t }) => t('version:versions'),
|
||||
|
||||
@@ -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: Permissions
|
||||
permissions: SanitizedPermissions
|
||||
}> = (props) => {
|
||||
const { collectionConfig, globalConfig, hideTabs, i18n, payload, permissions } = props
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { I18n, I18nClient } from '@payloadcms/translations'
|
||||
import type { PayloadRequest, Permissions, SanitizedConfig, User } from 'payload'
|
||||
import type { PayloadRequest, SanitizedConfig, SanitizedPermissions, 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: Permissions
|
||||
permissions: SanitizedPermissions
|
||||
req: PayloadRequest
|
||||
user: User
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const LocaleSelector: React.FC<{
|
||||
options: localeOptions,
|
||||
}}
|
||||
onChange={(value: string) => onChange(value)}
|
||||
path="locale"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ export const APIViewClient: React.FC = () => {
|
||||
label: t('version:draft'),
|
||||
}}
|
||||
onChange={() => setDraft(!draft)}
|
||||
path="draft"
|
||||
/>
|
||||
)}
|
||||
<CheckboxField
|
||||
@@ -167,6 +168,7 @@ export const APIViewClient: React.FC = () => {
|
||||
label: t('authentication:authenticated'),
|
||||
}}
|
||||
onChange={() => setAuthenticated(!authenticated)}
|
||||
path="authenticated"
|
||||
/>
|
||||
</div>
|
||||
{localeOptions && <LocaleSelector localeOptions={localeOptions} onChange={setLocale} />}
|
||||
@@ -181,6 +183,7 @@ export const APIViewClient: React.FC = () => {
|
||||
min: 0,
|
||||
}}
|
||||
onChange={(value) => setDepth(value?.toString())}
|
||||
path="depth"
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -36,6 +36,7 @@ export const ToggleTheme: React.FC = () => {
|
||||
],
|
||||
}}
|
||||
onChange={onChange}
|
||||
path="theme"
|
||||
value={autoMode ? 'auto' : theme}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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: DocumentPermissions
|
||||
docPermissions: SanitizedDocumentPermissions
|
||||
docPreferences: DocumentPreferences
|
||||
initialState: FormState
|
||||
loginWithUsername?: false | LoginWithUsernameOptions
|
||||
@@ -105,6 +105,7 @@ export const CreateFirstUserClient: React.FC<{
|
||||
label: t('authentication:newPassword'),
|
||||
required: true,
|
||||
}}
|
||||
path="password"
|
||||
/>
|
||||
<ConfirmPasswordField />
|
||||
<RenderFields
|
||||
@@ -113,7 +114,7 @@ export const CreateFirstUserClient: React.FC<{
|
||||
parentIndexPath=""
|
||||
parentPath=""
|
||||
parentSchemaPath={userSlug}
|
||||
permissions={null}
|
||||
permissions={true}
|
||||
readOnly={false}
|
||||
/>
|
||||
<FormSubmit size="large">{t('general:create')}</FormSubmit>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user