Files
payload/test/versions/int.spec.ts
Patrik ad553e967b fix: updates field validation error messages to use labels if applicable (#10601)
### What?

Previously, field error messages displayed in toast notifications used
the field path to reference fields that failed validation. This
path-based approach was necessary to distinguish between fields that
might share the same name when nested inside arrays, groups, rows, or
collapsible fields.

However, the human readability of these paths was lacking, especially
for unnamed fields like rows and collapsible fields. For example:

- A text field inside a row could display as: `_index-0.text`
- A text field nested within multiple arrays could display as:
`items.0.subArray.0.text`

These outputs are technically correct but not user-friendly.

### Why?

While the previous format was helpful for pinpointing the specific field
that caused the validation error, it could be more user-friendly and
clearer to read. The goal is to maintain the same level of accuracy
while improving the readability for both developers and content editors.

### How?

To improve readability, the following changes were made:

1. Use Field Labels Instead of Field Paths:
- The ValidationError component now uses the label prop from the field
config (if available) instead of the field’s name.
       - If a label is provided, it will be used in the error message.
       - If no label exists, it will fall back to the field’s name.

2. Remove _index from Paths for Unnamed Fields (In the validationError
component only):
- For unnamed fields like rows and collapsibles, the _index prefix is
now stripped from the output to make it cleaner.
       - Instead of `_index-0.text`, it now outputs just `Text`.

3. Reformat the Error Path for Readability:
- The error message format has been improved to be more human-readable,
showing the field hierarchy in a structured way with array indices
converted to 1-based numbers.

#### Example transformation:

##### Before:
The following fields are invalid: `items.0.subArray.0.text`

##### After:
The following fields are invalid: `Items 1 > SubArray 1 > Text`
2025-01-17 09:42:46 -05:00

2766 lines
74 KiB
TypeScript

import { createLocalReq, Payload } from 'payload'
import { schedulePublishHandler } from '@payloadcms/ui/utilities/schedulePublishHandler'
import path from 'path'
import { ValidationError } from 'payload'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
import { devUser } from '../credentials.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import { clearAndSeedEverything } from './clearAndSeedEverything.js'
import AutosavePosts from './collections/Autosave.js'
import AutosaveGlobal from './globals/Autosave.js'
import {
autosaveCollectionSlug,
autoSaveGlobalSlug,
draftCollectionSlug,
draftGlobalSlug,
localizedCollectionSlug,
localizedGlobalSlug,
} from './slugs.js'
let payload: Payload
let restClient: NextRESTClient
let collectionLocalPostID: string
let collectionLocalVersionID
let token
let collectionGraphQLPostID
let collectionGraphQLVersionID
const collectionGraphQLOriginalTitle = 'autosave title'
const collection = AutosavePosts.slug
const globalSlug = AutosaveGlobal.slug
let globalLocalVersionID
let globalGraphQLVersionID
const globalGraphQLOriginalTitle = 'updated global title'
const updatedTitle = 'Here is an updated post title in EN'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const formatGraphQLID = (id: number | string) =>
payload.db.defaultIDType === 'number' ? id : `"${id}"`
describe('Versions', () => {
let user
beforeAll(async () => {
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, restClient } = await initPayloadInt(dirname))
})
afterAll(async () => {
if (typeof payload.db.destroy === 'function') {
await payload.db.destroy()
}
})
beforeEach(async () => {
await clearAndSeedEverything(payload)
const login = `
mutation {
loginUser(
email: "${devUser.email}",
password: "${devUser.password}"
) {
token
user {
id
}
}
}`
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query: login }) })
.then((res) => res.json())
user = { ...data.loginUser.user, collection: 'users' }
token = data.loginUser.token
// now: initialize
const autosavePost = await payload.create({
collection,
data: {
description: '345j23o4ifj34jf54g',
title: 'Here is an autosave post in EN',
},
})
collectionLocalPostID = autosavePost.id
await payload.update({
id: collectionLocalPostID,
collection,
data: {
title: updatedTitle,
},
})
const versions = await payload.findVersions({
collection,
})
collectionLocalVersionID = versions.docs[0].id
})
describe('Collections - Local', () => {
describe('Create', () => {
it('should allow creating a draft with missing required field data', async () => {
const draft = await payload.create({
collection: autosaveCollectionSlug,
data: {
description: undefined,
title: 'i have a title',
},
draft: true,
})
expect(draft.id).toBeDefined()
})
it('should allow a new version to be created and updated', async () => {
const updatedPost = await payload.findByID({
id: collectionLocalPostID,
collection,
})
expect(updatedPost.title).toBe(updatedTitle)
expect(updatedPost._status).toStrictEqual('draft')
expect(collectionLocalVersionID).toBeDefined()
})
it('should paginate versions', async () => {
const versions = await payload.findVersions({
collection: draftCollectionSlug,
limit: 5,
})
const versionsPage2 = await payload.findVersions({
collection: draftCollectionSlug,
limit: 5,
page: 2,
})
expect(versions.docs).toHaveLength(5)
expect(versions.page).toBe(1)
expect(versionsPage2.docs).toHaveLength(5)
expect(versionsPage2.page).toBe(2)
expect(versions.docs[0].id).not.toBe(versionsPage2.docs[0].id)
})
it('should allow saving multiple versions of models with unique fields', async () => {
const autosavePost = await payload.create({
collection,
data: {
description: 'description 1',
title: 'unique unchanging title',
},
})
await payload.update({
id: autosavePost.id,
collection,
data: {
description: 'description 2',
},
})
const finalDescription = 'final description'
const secondUpdate = await payload.update({
id: autosavePost.id,
collection,
data: {
description: finalDescription,
},
})
expect(secondUpdate.description).toBe(finalDescription)
})
it('should allow a version to be retrieved by ID', async () => {
const version = await payload.findVersionByID({
id: collectionLocalVersionID,
collection,
})
expect(version.id).toStrictEqual(collectionLocalVersionID)
})
it('should allow a version to save locales properly', async () => {
const englishTitle = 'Title in EN'
const spanishTitle = 'Title in ES'
await payload.update({
id: collectionLocalPostID,
collection,
data: {
title: englishTitle,
},
})
const updatedPostES = await payload.update({
id: collectionLocalPostID,
collection,
data: {
title: spanishTitle,
},
locale: 'es',
})
expect(updatedPostES.title).toBe(spanishTitle)
const newEnglishTitle = 'New title in EN'
await payload.update({
id: collectionLocalPostID,
collection,
data: {
title: newEnglishTitle,
},
})
const versions = await payload.findVersions({
collection,
locale: 'all',
where: {
parent: {
equals: collectionLocalPostID,
},
},
})
expect(versions.docs[0].version.title.en).toStrictEqual(newEnglishTitle)
expect(versions.docs[0].version.title.es).toStrictEqual(spanishTitle)
})
it('should query drafts with sort', async () => {
const draftsAscending = await payload.find({
collection: draftCollectionSlug,
draft: true,
sort: 'title',
})
const draftsDescending = await payload.find({
collection: draftCollectionSlug,
draft: true,
sort: '-title',
})
expect(draftsAscending).toBeDefined()
expect(draftsDescending).toBeDefined()
expect(draftsAscending.docs[0]).toMatchObject(
draftsDescending.docs[draftsDescending.docs.length - 1],
)
})
// https://github.com/payloadcms/payload/issues/4827
it('should query drafts with relation', async () => {
const draftPost = await payload.create({
collection: draftCollectionSlug,
data: {
description: 'Description',
title: 'Some Title',
},
})
await payload.create({
collection: draftCollectionSlug,
data: {
description: 'Description',
relation: draftPost.id,
title: 'With Relation',
},
})
const query = {
collection: draftCollectionSlug,
where: {
relation: {
equals: draftPost.id,
},
},
}
const all = await payload.find(query)
const drafts = await payload.find({ ...query, draft: true })
expect(all.docs).toHaveLength(1)
expect(drafts.docs).toHaveLength(1)
})
it('should `findVersions` with sort', async () => {
const draftsAscending = await payload.findVersions({
collection: draftCollectionSlug,
draft: true,
limit: 100,
sort: 'createdAt',
})
const draftsDescending = await payload.findVersions({
collection: draftCollectionSlug,
draft: true,
limit: 100,
sort: '-createdAt',
})
expect(draftsAscending).toBeDefined()
expect(draftsDescending).toBeDefined()
expect(draftsAscending.docs[0]).toMatchObject(
draftsDescending.docs[draftsDescending.docs.length - 1],
)
})
it('should have different createdAt in a new version while the same version.createdAt', async () => {
const doc = await payload.create({
collection: autosaveCollectionSlug,
data: { description: 'descr', title: 'title' },
})
await wait(10)
const upd = await payload.update({
collection: autosaveCollectionSlug,
id: doc.id,
data: {},
})
expect(upd.createdAt).toBe(doc.createdAt)
const {
docs: [latestVersionData],
} = await payload.findVersions({
collection: autosaveCollectionSlug,
where: {
and: [
{
parent: {
equals: doc.id,
},
latest: {
equals: true,
},
},
],
},
})
// Version itself should have new createdAt
expect(new Date(latestVersionData.createdAt) > new Date(doc.createdAt)).toBe(true)
// But the same createdAt in version data!
expect(latestVersionData.version.createdAt).toBe(doc.createdAt)
const fromNonVersionsTable = await payload.findByID({
draft: false,
id: doc.id,
collection: autosaveCollectionSlug,
})
// createdAt from non-versions should be the same as version_createdAt in versions
expect(fromNonVersionsTable.createdAt).toBe(latestVersionData.version.createdAt)
// When creating new version - updatedAt should match version.updatedAt
expect(fromNonVersionsTable.updatedAt).toBe(latestVersionData.version.updatedAt)
})
})
describe('Restore', () => {
it('should return `findVersions` in correct order', async () => {
const somePost = await payload.create({
collection: draftCollectionSlug,
data: {
description: 'description 1',
title: 'first post',
},
})
const updatedPost = await payload.update({
id: somePost.id,
collection: draftCollectionSlug,
data: {
title: 'This should be the latest version',
},
})
const versions = await payload.findVersions({
collection: draftCollectionSlug,
where: {
parent: { equals: somePost.id },
},
})
expect(versions.docs[0].version.title).toBe(updatedPost.title)
})
it('should allow a version to be restored', async () => {
const title2 = 'Another updated post title in EN'
const updated = 'updated'
const versionedPost = await payload.create({
collection: draftCollectionSlug,
data: {
description: 'version description',
title: 'version title',
},
draft: true,
})
let updatedPost = await payload.update({
id: versionedPost.id,
collection: draftCollectionSlug,
data: {
blocksField: [
{
blockType: 'block',
localized: 'text',
text: 'text',
},
],
title: title2,
},
draft: true,
})
updatedPost = await payload.update({
id: versionedPost.id,
collection: draftCollectionSlug,
data: {
blocksField: [
{
id: updatedPost.blocksField[0].id,
blockName: 'breakpoint',
blockType: 'block',
localized: updated,
text: updated,
},
],
title: title2,
},
draft: true,
})
expect(updatedPost.title).toBe(title2)
expect(updatedPost.blocksField[0].text).toBe(updated)
expect(updatedPost.blocksField[0].localized).toBe(updated)
// Make sure it was updated correctly
const draftFromUpdatedPost = await payload.findByID({
id: versionedPost.id,
collection: draftCollectionSlug,
draft: true,
})
expect(draftFromUpdatedPost.title).toBe(title2)
expect(draftFromUpdatedPost.blocksField).toHaveLength(1)
expect(draftFromUpdatedPost.blocksField[0].localized).toStrictEqual(updated)
const versions = await payload.findVersions({
collection: draftCollectionSlug,
where: {
parent: {
equals: versionedPost.id,
},
},
})
const versionToRestore = versions.docs[versions.docs.length - 1]
// restore to previous version
const restoredVersion = await payload.restoreVersion({
id: versionToRestore.id,
collection: draftCollectionSlug,
})
expect({ ...restoredVersion }).toMatchObject({
...versionToRestore.version,
updatedAt: restoredVersion.updatedAt,
})
const latestDraft = await payload.findByID({
id: versionedPost.id,
collection: draftCollectionSlug,
draft: true,
})
expect(latestDraft).toMatchObject({
...versionToRestore.version,
// timestamps cannot be guaranteed to be the exact same to the milliseconds
createdAt: latestDraft.createdAt,
updatedAt: latestDraft.updatedAt,
})
expect(latestDraft.blocksField).toHaveLength(0)
})
})
it('should restore published version with correct data', async () => {
// create a post
const originalPost = await payload.create({
collection: draftCollectionSlug,
data: {
description: 'description',
title: 'v1',
_status: 'published',
},
})
// update the post
await payload.update({
collection: draftCollectionSlug,
draft: true,
id: originalPost.id,
data: {
title: 'v2',
_status: 'published',
},
})
// get the version id of the original draft
const versions = await payload.findVersions({
collection: draftCollectionSlug,
where: {
parent: {
equals: originalPost.id,
},
},
})
// restore the version
const versionToRestore = versions.docs[versions.docs.length - 1]
const restoredVersion = await payload.restoreVersion({
id: versionToRestore.id,
collection: draftCollectionSlug,
})
// get the latest draft
const latestDraft = await payload.findByID({
id: originalPost.id,
collection: draftCollectionSlug,
draft: true,
})
// assert it has the original post content
expect(latestDraft.title).toStrictEqual('v1')
expect(restoredVersion.title).toStrictEqual('v1')
})
describe('Update', () => {
it('should allow a draft to be patched', async () => {
const originalTitle = 'Here is a published post'
const originalPublishedPost = await payload.create({
collection,
data: {
_status: 'published',
description: 'kjnjyhbbdsfseankuhsjsfghb',
title: originalTitle,
},
})
const patchedTitle = 'Here is a draft post with a patched title'
await payload.update({
id: originalPublishedPost.id,
collection,
data: {
_status: 'draft',
title: patchedTitle,
},
draft: true,
locale: 'en',
})
const spanishTitle = 'es title'
// second update to existing draft
await payload.update({
id: originalPublishedPost.id,
collection,
data: {
_status: 'draft',
title: spanishTitle,
},
draft: true,
locale: 'es',
})
const publishedPost = await payload.findByID({
id: originalPublishedPost.id,
collection,
})
const draftPost = await payload.findByID({
id: originalPublishedPost.id,
collection,
draft: true,
locale: 'all',
})
expect(publishedPost.title).toBe(originalTitle)
expect(draftPost.title.en).toBe(patchedTitle)
expect(draftPost.title.es).toBe(spanishTitle)
})
it('should have correct updatedAt timestamps when saving drafts', async () => {
const created = await payload.create({
collection: draftCollectionSlug,
data: {
description: 'desc',
title: 'title',
},
draft: true,
})
await wait(10)
const updated = await payload.update({
id: created.id,
collection: draftCollectionSlug,
data: {
title: 'updated title',
},
draft: true,
})
const createdUpdatedAt = new Date(created.updatedAt)
const updatedUpdatedAt = new Date(updated.updatedAt)
expect(Number(updatedUpdatedAt)).toBeGreaterThan(Number(createdUpdatedAt))
})
it('should have correct updatedAt timestamps when saving drafts with autosave', async () => {
const created = await payload.create({
collection: draftCollectionSlug,
data: {
description: 'desc',
title: 'title',
},
draft: true,
})
await wait(10)
const updated = await payload.update({
id: created.id,
collection: draftCollectionSlug,
data: {
title: 'updated title',
},
draft: true,
autosave: true,
})
const createdUpdatedAt = new Date(created.updatedAt)
const updatedUpdatedAt = new Date(updated.updatedAt)
expect(Number(updatedUpdatedAt)).toBeGreaterThan(Number(createdUpdatedAt))
})
it('should validate when publishing with the draft arg', async () => {
// no title (not valid for publishing)
const doc = await payload.create({
collection: draftCollectionSlug,
data: {
description: 'desc',
},
draft: true,
})
await expect(async () => {
// should not be able to publish a doc that fails validation
await payload.update({
id: doc.id,
collection: draftCollectionSlug,
data: { _status: 'published' },
draft: true,
})
}).rejects.toThrow(ValidationError)
// succeeds but returns zero docs updated, with an error
const updateManyResult = await payload.update({
collection: draftCollectionSlug,
data: { _status: 'published' },
draft: true,
where: {
id: { equals: doc.id },
},
})
expect(updateManyResult.docs).toHaveLength(0)
expect(updateManyResult.errors).toStrictEqual([
{ id: doc.id, message: 'The following field is invalid: Title' },
])
})
})
describe('Update Many', () => {
it('should update many using drafts', async () => {
const doc = await payload.create({
collection: draftCollectionSlug,
data: {
_status: 'published',
description: 'description to bulk update',
title: 'initial value',
},
})
await payload.update({
id: doc.id,
collection: draftCollectionSlug,
data: {
title: 'updated title',
},
draft: true,
})
// bulk publish
const updated = await payload.update({
collection: draftCollectionSlug,
data: {
_status: 'published',
description: 'updated description',
},
draft: true,
where: {
id: {
in: [doc.id],
},
},
})
const updatedDoc = updated.docs?.[0]
// get the published doc
const findResult = await payload.find({
collection: draftCollectionSlug,
where: {
id: { equals: doc.id },
},
})
const findDoc = findResult.docs?.[0]
expect(updatedDoc.description).toStrictEqual('updated description')
expect(updatedDoc.title).toStrictEqual('updated title')
expect(findDoc.title).toStrictEqual('updated title')
expect(findDoc.description).toStrictEqual('updated description')
})
})
describe('Delete', () => {
let postToDelete
beforeEach(async () => {
postToDelete = await payload.create({
collection,
data: {
_status: 'draft',
description: 'description',
title: 'title to delete',
},
})
})
it('should delete drafts', async () => {
const drafts = await payload.db.queryDrafts({
collection,
where: {
parent: {
equals: postToDelete.id,
},
},
})
await payload.delete({
collection,
where: {
id: { equals: postToDelete.id },
},
})
const result = await payload.db.queryDrafts({
collection,
where: {
parent: {
in: drafts.docs.map(({ id }) => id),
},
// appendVersionToQueryKey,
},
})
expect(result.docs).toHaveLength(0)
})
})
describe('Draft Count', () => {
it('creates proper number of drafts', async () => {
const originalDraft = await payload.create({
collection: 'draft-posts',
data: {
_status: 'draft',
description: 'A',
title: 'A',
},
draft: true,
})
await payload.update({
id: originalDraft.id,
collection: 'draft-posts',
data: {
_status: 'draft',
description: 'B',
title: 'B',
},
draft: true,
})
await payload.update({
id: originalDraft.id,
collection: 'draft-posts',
data: {
_status: 'draft',
description: 'C',
title: 'C',
},
draft: true,
})
const mostRecentDraft = await payload.findByID({
id: originalDraft.id,
collection: 'draft-posts',
draft: true,
})
expect(mostRecentDraft.title).toStrictEqual('C')
const versions = await payload.findVersions({
collection: 'draft-posts',
where: {
parent: {
equals: originalDraft.id,
},
},
})
expect(versions.docs).toHaveLength(3)
})
})
describe('Max Versions', () => {
// create 2 documents with 3 versions each
// expect 2 documents with 2 versions each
it('retains correct versions', async () => {
const doc1 = await payload.create({
collection: 'version-posts',
data: {
description: 'A',
title: 'A',
},
})
await payload.update({
id: doc1.id,
collection: 'version-posts',
data: {
description: 'B',
title: 'B',
},
})
const result = await payload.find({
collection: 'version-posts',
where: {
updatedAt: { less_than_equal: '2027-01-01T00:00:00.000Z' },
},
})
await payload.update({
id: doc1.id,
collection: 'version-posts',
data: {
description: 'C',
title: 'C',
},
})
const doc2 = await payload.create({
collection: 'version-posts',
data: {
description: 'D',
title: 'D',
},
})
await payload.update({
id: doc2.id,
collection: 'version-posts',
data: {
description: 'E',
title: 'E',
},
})
await payload.update({
id: doc2.id,
collection: 'version-posts',
data: {
description: 'F',
title: 'F',
},
})
const doc1Versions = await payload.findVersions({
collection: 'version-posts',
sort: '-updatedAt',
where: {
parent: {
equals: doc1.id,
},
},
})
const doc2Versions = await payload.findVersions({
collection: 'version-posts',
sort: '-updatedAt',
where: {
parent: {
equals: doc2.id,
},
},
})
// correctly retains 2 documents in the versions collection
expect(doc1Versions.totalDocs).toStrictEqual(2)
// correctly retains the most recent 2 versions
expect(doc1Versions.docs[1].version.title).toStrictEqual('B')
// correctly retains 2 documents in the versions collection
expect(doc2Versions.totalDocs).toStrictEqual(2)
// correctly retains the most recent 2 versions
expect(doc2Versions.docs[1].version.title).toStrictEqual('E')
const docs = await payload.find({
collection: 'version-posts',
})
// correctly retains 2 documents in the actual collection
expect(docs.totalDocs).toStrictEqual(2)
})
})
describe('Race conditions', () => {
it('should keep latest true with parallel writes', async () => {
const doc = await payload.create({
collection: 'draft-posts',
data: {
description: 'A',
title: 'A',
},
})
const writeAmount = 100
const promises = Array.from({ length: writeAmount }, async (_, i) => {
return new Promise((resolve) => {
// Add latency so updates aren't immediate after each other but still in parallel
setTimeout(() => {
payload
.update({
id: doc.id,
collection: 'draft-posts',
data: {},
draft: true,
})
.then(resolve)
.catch(resolve)
}, i * 5)
})
})
await Promise.all(promises)
const { docs } = await payload.findVersions({
collection: 'draft-posts',
where: {
and: [
{
parent: {
equals: doc.id,
},
},
{
latest: {
equals: true,
},
},
],
},
})
expect(docs[0]).toBeDefined()
})
})
})
describe('Querying', () => {
const originalTitle = 'original title'
const updatedTitle1 = 'new title 1'
const updatedTitle2 = 'new title 2'
let firstDraft
beforeEach(async () => {
// This will be created in the `draft-posts` collection
firstDraft = await payload.create({
collection: 'draft-posts',
data: {
description: 'my description',
radio: 'test',
title: originalTitle,
},
})
// This will be created in the `_draft-posts_versions` collection
await payload.update({
id: firstDraft.id,
collection: 'draft-posts',
data: {
title: updatedTitle1,
},
draft: true,
})
// This will be created in the `_draft-posts_versions` collection
// and will be the newest draft, able to be queried on
await payload.update({
id: firstDraft.id,
collection: 'draft-posts',
data: {
title: updatedTitle2,
},
draft: true,
})
})
it('should allow querying a draft doc from main collection', async () => {
const findResults = await payload.find({
collection: 'draft-posts',
where: {
title: {
equals: originalTitle,
},
},
})
expect(findResults.docs[0].title).toStrictEqual(originalTitle)
})
it('should return more than 10 `totalDocs`', async () => {
const { id } = await payload.create({
collection: 'draft-posts',
data: {
description: 'Description',
title: 'Title',
},
})
const createVersions = async (int: number = 1) => {
for (let i = 0; i < int; i++) {
await payload.update({
id,
collection: 'draft-posts',
data: {
title: `Title ${i}`,
},
})
}
}
await createVersions(10)
const findResults = await payload.findVersions({
collection: 'draft-posts',
where: {
parent: {
equals: id,
},
},
})
expect(findResults.totalDocs).toBe(11)
})
it('should not be able to query an old draft version with draft=true', async () => {
const draftFindResults = await payload.find({
collection: 'draft-posts',
draft: true,
where: {
title: {
equals: updatedTitle1,
},
},
})
expect(draftFindResults.docs).toHaveLength(0)
})
it('should be able to query the newest draft version with draft=true', async () => {
const draftFindResults = await payload.find({
collection: 'draft-posts',
draft: true,
where: {
title: {
equals: updatedTitle2,
},
},
})
expect(draftFindResults.docs[0].title).toStrictEqual(updatedTitle2)
})
it("should not be able to query old drafts that don't match with draft=true", async () => {
const draftFindResults = await payload.find({
collection: 'draft-posts',
draft: true,
where: {
title: {
equals: originalTitle,
},
},
})
expect(draftFindResults.docs).toHaveLength(0)
})
it('should be able to query by id with draft=true', async () => {
const allDocs = await payload.find({
collection: 'draft-posts',
draft: true,
})
expect(allDocs.docs.length).toBeGreaterThan(1)
const byID = await payload.find({
collection: 'draft-posts',
draft: true,
where: {
id: {
equals: allDocs.docs[0].id,
},
},
})
expect(byID.docs).toHaveLength(1)
})
it('should be able to query by id AND any other field with draft=true', async () => {
const allDocs = await payload.find({
collection: 'draft-posts',
draft: true,
})
expect(allDocs.docs.length).toBeGreaterThan(1)
const results = await payload.find({
collection: 'draft-posts',
draft: true,
where: {
and: [
{
id: {
not_in: allDocs.docs[0].id,
},
},
{
title: {
like: 'Published',
},
},
],
},
})
expect(results.docs).toHaveLength(1)
})
})
describe('Collections - GraphQL', () => {
beforeEach(async () => {
const description = 'autosave description'
const query = `mutation {
createAutosavePost(data: {title: "${collectionGraphQLOriginalTitle}", description: "${description}"}) {
id
title
description
createdAt
updatedAt
_status
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
collectionGraphQLPostID = data.createAutosavePost.id
})
describe('Create', () => {
it('should allow a new doc to be created with draft status', async () => {
const description2 = 'other autosave description'
const query = `mutation {
createAutosavePost(data: {title: "${'Some other title'}", description: "${description2}"}) {
id
title
description
createdAt
updatedAt
_status
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
expect(data.createAutosavePost._status).toStrictEqual('draft')
})
})
describe('Read', () => {
const updatedTitle2 = 'updated title'
beforeEach(async () => {
// modify the post to create a new version
// language=graphQL
const update = `mutation {
updateAutosavePost(id: ${formatGraphQLID(
collectionGraphQLPostID,
)}, data: {title: "${updatedTitle2}"}) {
title
updatedAt
createdAt
}
}`
await restClient.GRAPHQL_POST({
body: JSON.stringify({ query: update }),
headers: {
Authorization: `JWT ${token}`,
},
})
// language=graphQL
const query = `query {
versionsAutosavePosts(where: { parent: { equals: ${formatGraphQLID(
collectionGraphQLPostID,
)} } }) {
docs {
id
}
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
collectionGraphQLVersionID = data.versionsAutosavePosts.docs[0].id
})
it('should allow read of versions by version id', async () => {
const query = `query {
versionAutosavePost(id: ${formatGraphQLID(collectionGraphQLVersionID)}) {
id
parent {
id
}
version {
title
}
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
expect(data.versionAutosavePost.id).toBeDefined()
expect(data.versionAutosavePost.parent.id).toStrictEqual(collectionGraphQLPostID)
expect(data.versionAutosavePost.version.title).toStrictEqual(updatedTitle2)
})
it('should allow read of versions by querying version content', async () => {
// language=graphQL
const query = `query {
versionsAutosavePosts(where: { version__title: {equals: "${collectionGraphQLOriginalTitle}" } }) {
docs {
id
parent {
id
}
version {
title
}
}
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
const doc = data.versionsAutosavePosts.docs[0]
expect(doc.id).toBeDefined()
expect(doc.parent.id).toStrictEqual(collectionGraphQLPostID)
expect(doc.version.title).toStrictEqual(collectionGraphQLOriginalTitle)
})
})
describe('Restore', () => {
beforeEach(async () => {
// modify the post to create a new version
// language=graphQL
const update = `mutation {
updateAutosavePost(id: ${formatGraphQLID(
collectionGraphQLPostID,
)}, data: {title: "${collectionGraphQLOriginalTitle}"}) {
title
updatedAt
createdAt
}
}`
await restClient.GRAPHQL_POST({
body: JSON.stringify({ query: update }),
headers: {
Authorization: `JWT ${token}`,
},
})
// language=graphQL
const query = `query {
versionsAutosavePosts(where: { parent: { equals: ${formatGraphQLID(
collectionGraphQLPostID,
)} } }) {
docs {
id
}
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
collectionGraphQLVersionID = data.versionsAutosavePosts.docs[0].id
})
it('should allow a version to be restored', async () => {
// Update it
const update = `mutation {
updateAutosavePost(id: ${formatGraphQLID(
collectionGraphQLPostID,
)}, data: {title: "${'Wrong title'}"}) {
title
updatedAt
createdAt
}
}`
await restClient.GRAPHQL_POST({
body: JSON.stringify({ query: update }),
headers: {
Authorization: `JWT ${token}`,
},
})
// restore a versionsPost
const restore = `mutation {
restoreVersionAutosavePost(id: ${formatGraphQLID(collectionGraphQLVersionID)}) {
title
}
}`
await restClient.GRAPHQL_POST({
body: JSON.stringify({ query: restore }),
headers: {
Authorization: `JWT ${token}`,
},
})
const query = `query {
AutosavePost(id: ${formatGraphQLID(collectionGraphQLPostID)}) {
title
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
expect(data.AutosavePost.title).toStrictEqual(collectionGraphQLOriginalTitle)
})
})
})
describe('Collections - REST', () => {
it('sholud query versions', async () => {
const response = await restClient.GET(`/${collection}/versions`)
expect(response.status).toBe(200)
const json = await response.json()
expect(json.docs[0].parent).toBe(collectionLocalPostID)
const responseByID = await restClient.GET(`/${collection}/versions/${json.docs[0].id}`)
expect(responseByID.status).toBe(200)
const jsonByID = await responseByID.json()
expect(jsonByID.parent).toBe(collectionLocalPostID)
})
})
describe('Globals - Local', () => {
beforeEach(async () => {
const title2 = 'Here is an updated global title in EN'
await payload.updateGlobal({
slug: globalSlug,
data: {
title: 'Test Global',
},
})
await payload.updateGlobal({
slug: globalSlug,
data: {
title: title2,
},
})
const versions = await payload.findGlobalVersions({
slug: globalSlug,
})
globalLocalVersionID = versions.docs[0].id
})
describe('Create', () => {
it('should allow a new version to be created', async () => {
const title2 = 'Here is an updated global title in EN'
const updatedGlobal = await payload.findGlobal({
slug: globalSlug,
})
expect(updatedGlobal.title).toBe(title2)
expect(updatedGlobal._status).toStrictEqual('draft')
expect(globalLocalVersionID).toBeDefined()
})
it('should have different createdAt in a new version while the same version.createdAt', async () => {
const doc = await payload.updateGlobal({
slug: autoSaveGlobalSlug,
data: { title: 'asd' },
})
await wait(10)
const upd = await payload.updateGlobal({
slug: autoSaveGlobalSlug,
data: { title: 'asd2' },
})
expect(upd.createdAt).toBe(doc.createdAt)
const {
docs: [latestVersionData],
} = await payload.findGlobalVersions({
slug: autoSaveGlobalSlug,
where: {
latest: {
equals: true,
},
},
})
// Version itself should have new createdAt
expect(new Date(latestVersionData.createdAt) > new Date(doc.createdAt)).toBe(true)
// But the same version.createdAt!
expect(latestVersionData.version.createdAt).toBe(doc.createdAt)
const fromNonVersionsTable = await payload.findGlobal({
draft: false,
slug: autoSaveGlobalSlug,
})
// createdAt from non-versions should be the same as version_createdAt in versions
expect(fromNonVersionsTable.createdAt).toBe(latestVersionData.version.createdAt)
// When creating a new version - updatedAt should match
expect(fromNonVersionsTable.updatedAt).toBe(latestVersionData.version.updatedAt)
})
})
describe('Read', () => {
it('should allow a version to be retrieved by ID', async () => {
const version = await payload.findGlobalVersionByID({
id: globalLocalVersionID,
slug: globalSlug,
})
expect(version.id).toStrictEqual(globalLocalVersionID)
})
})
describe('Update', () => {
it('should allow a version to save locales properly', async () => {
const englishTitle = 'Title in EN'
const spanishTitle = 'Title in ES'
await payload.updateGlobal({
slug: globalSlug,
data: {
title: englishTitle,
},
})
const updatedGlobalES = await payload.updateGlobal({
slug: globalSlug,
data: {
title: spanishTitle,
},
locale: 'es',
})
expect(updatedGlobalES.title).toBe(spanishTitle)
const newEnglishTitle = 'New title in EN'
await payload.updateGlobal({
slug: globalSlug,
data: {
title: newEnglishTitle,
},
})
const versions = await payload.findGlobalVersions({
slug: globalSlug,
locale: 'all',
})
expect(versions.docs[0].version.title.en).toStrictEqual(newEnglishTitle)
expect(versions.docs[0].version.title.es).toStrictEqual(spanishTitle)
})
})
describe('Restore', () => {
it('should allow a version to be restored', async () => {
const title2 = 'Another updated title in EN'
const updatedGlobal = await payload.updateGlobal({
slug: globalSlug,
data: {
title: title2,
},
})
expect(updatedGlobal.title).toBe(title2)
// Make sure it was updated correctly
const foundUpdatedGlobal = await payload.findGlobal({
slug: globalSlug,
draft: true,
})
expect(foundUpdatedGlobal.title).toBe(title2)
const versions = await payload.findGlobalVersions({
slug: globalSlug,
})
globalLocalVersionID = versions.docs[1].id
const restore = await payload.restoreGlobalVersion({
id: globalLocalVersionID,
slug: globalSlug,
})
expect(restore.version.title).toBeDefined()
const restoredGlobal = await payload.findGlobal({
slug: globalSlug,
draft: true,
})
expect(restoredGlobal.title).toBe(restore.version.title.en)
})
})
describe('Patch', () => {
it('should allow a draft to be patched', async () => {
const originalTitle = 'Here is a published global'
await payload.updateGlobal({
slug: globalSlug,
data: {
_status: 'published',
description: 'kjnjyhbbdsfseankuhsjsfghb',
title: originalTitle,
},
})
const publishedGlobal = await payload.findGlobal({
slug: globalSlug,
draft: true,
})
const updatedTitle2 = 'Here is a draft global with a patched title'
await payload.updateGlobal({
slug: globalSlug,
data: {
_status: 'draft',
title: updatedTitle2,
},
draft: true,
locale: 'en',
})
await payload.updateGlobal({
slug: globalSlug,
data: {
_status: 'draft',
title: updatedTitle2,
},
draft: true,
locale: 'es',
})
const updatedGlobal = await payload.findGlobal({
slug: globalSlug,
draft: true,
locale: 'all',
})
expect(publishedGlobal.title).toBe(originalTitle)
expect(updatedGlobal.title.en).toBe(updatedTitle2)
expect(updatedGlobal.title.es).toBe(updatedTitle2)
})
it('should allow a draft to be published', async () => {
const originalTitle = 'Here is a draft'
await payload.updateGlobal({
slug: globalSlug,
data: {
_status: 'draft',
title: originalTitle,
},
draft: true,
})
const updatedTitle2 = 'Now try to publish'
const result = await payload.updateGlobal({
slug: globalSlug,
data: {
_status: 'published',
title: updatedTitle2,
},
})
expect(result.title).toBe(updatedTitle2)
})
})
})
describe('Globals - GraphQL', () => {
beforeEach(async () => {
// language=graphql
const update = `mutation {
updateAutosaveGlobal(draft: true, data: {
title: "${globalGraphQLOriginalTitle}"
}) {
_status
title
}
}`
await restClient.GRAPHQL_POST({
body: JSON.stringify({ query: update }),
headers: {
Authorization: `JWT ${token}`,
},
})
// language=graphQL
const query = `query {
versionsAutosaveGlobal(where: { version__title: { equals: "${globalGraphQLOriginalTitle}" } }) {
docs {
id
version {
title
}
}
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
globalGraphQLVersionID = data.versionsAutosaveGlobal.docs[0].id
})
describe('Read', () => {
it('should allow read of versions by version id', async () => {
// language=graphql
const query = `query {
versionAutosaveGlobal(id: ${formatGraphQLID(globalGraphQLVersionID)}) {
id
version {
title
}
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
expect(data.versionAutosaveGlobal.id).toBeDefined()
expect(data.versionAutosaveGlobal.version.title).toStrictEqual(globalGraphQLOriginalTitle)
})
it('should allow read of versions by querying version content', async () => {
// language=graphQL
const query = `query {
versionsAutosaveGlobal(where: { version__title: {equals: "${globalGraphQLOriginalTitle}" } }) {
docs {
id
version {
title
}
}
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
const doc = data.versionsAutosaveGlobal.docs[0]
expect(doc.id).toBeDefined()
expect(doc.version.title).toStrictEqual(globalGraphQLOriginalTitle)
})
})
describe('Restore', () => {
it('should allow a version to be restored', async () => {
// language=graphql
const restore = `mutation {
restoreVersionAutosaveGlobal(id: ${formatGraphQLID(globalGraphQLVersionID)}) {
title
}
}`
await restClient.GRAPHQL_POST({
body: JSON.stringify({ query: restore }),
headers: {
Authorization: `JWT ${token}`,
},
})
const query = `query {
AutosaveGlobal {
title
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
expect(data.AutosaveGlobal).toEqual({ title: globalGraphQLOriginalTitle })
})
})
})
describe('Scheduled Publish', () => {
it('should allow collection scheduled publish', async () => {
const draft = await payload.create({
collection: draftCollectionSlug,
data: {
title: 'my doc to publish in the future',
description: 'hello',
},
draft: true,
})
expect(draft._status).toStrictEqual('draft')
const currentDate = new Date()
await payload.jobs.queue({
task: 'schedulePublish',
waitUntil: new Date(currentDate.getTime() + 3000),
input: {
doc: {
relationTo: draftCollectionSlug,
value: draft.id,
},
},
})
await wait(4000)
await payload.jobs.run()
const retrieved = await payload.findByID({
collection: draftCollectionSlug,
id: draft.id,
})
expect(retrieved._status).toStrictEqual('published')
})
it('should restrict scheduled publish based on user', async () => {
const draft = await payload.create({
collection: draftCollectionSlug,
data: {
title: 'my doc to publish in the future',
description: 'hello',
restrictedToUpdate: true,
},
draft: true,
})
expect(draft._status).toStrictEqual('draft')
const currentDate = new Date()
await payload.jobs.queue({
task: 'schedulePublish',
waitUntil: new Date(currentDate.getTime() + 3000),
input: {
doc: {
relationTo: draftCollectionSlug,
value: draft.id,
},
user: user.id,
},
})
await wait(4000)
const res = await payload.jobs.run()
expect(res.jobStatus[Object.keys(res.jobStatus)[0]].status).toBe('error-reached-max-retries')
const retrieved = await payload.findByID({
collection: draftCollectionSlug,
id: draft.id,
})
expect(retrieved._status).toStrictEqual('draft')
})
it('should allow collection scheduled unpublish', async () => {
const published = await payload.create({
collection: draftCollectionSlug,
data: {
title: 'my doc to publish in the future',
description: 'hello',
_status: 'published',
},
draft: true,
})
expect(published._status).toStrictEqual('published')
const currentDate = new Date()
await payload.jobs.queue({
task: 'schedulePublish',
waitUntil: new Date(currentDate.getTime() + 3000),
input: {
type: 'unpublish',
doc: {
relationTo: draftCollectionSlug,
value: published.id,
},
},
})
await wait(4000)
await payload.jobs.run()
const retrieved = await payload.findByID({
collection: draftCollectionSlug,
id: published.id,
})
expect(retrieved._status).toStrictEqual('draft')
})
it('should delete scheduled jobs after a document is deleted', async () => {
const draft = await payload.create({
collection: draftCollectionSlug,
data: {
title: 'my doc to publish in the future',
description: 'hello',
},
draft: true,
})
expect(draft._status).toStrictEqual('draft')
const currentDate = new Date()
await payload.jobs.queue({
task: 'schedulePublish',
waitUntil: new Date(currentDate.getTime() + 3000),
input: {
type: 'publish',
doc: {
relationTo: draftCollectionSlug,
value: draft.id,
},
},
})
await payload.delete({
collection: draftCollectionSlug,
where: {
id: { equals: draft.id },
},
})
const { docs } = await payload.find({
collection: 'payload-jobs',
where: {
'input.doc.value': {
equals: draft.id,
},
},
})
expect(docs[0]).toBeUndefined()
})
it('should delete scheduled jobs after a document is deleted by ID', async () => {
const draft = await payload.create({
collection: draftCollectionSlug,
data: {
title: 'my doc to publish in the future',
description: 'hello',
},
draft: true,
})
expect(draft._status).toStrictEqual('draft')
const currentDate = new Date()
await payload.jobs.queue({
task: 'schedulePublish',
waitUntil: new Date(currentDate.getTime() + 3000),
input: {
type: 'publish',
doc: {
relationTo: draftCollectionSlug,
value: draft.id,
},
},
})
await payload.delete({
collection: draftCollectionSlug,
id: draft.id,
})
const { docs } = await payload.find({
collection: 'payload-jobs',
where: {
'input.doc.value': {
equals: draft.id,
},
},
})
expect(docs[0]).toBeUndefined()
})
it('should allow global scheduled publish', async () => {
const draft = await payload.updateGlobal({
slug: draftGlobalSlug,
data: {
_status: 'draft',
title: 'i will publish',
},
draft: true,
})
expect(draft._status).toStrictEqual('draft')
const currentDate = new Date()
await payload.jobs.queue({
task: 'schedulePublish',
waitUntil: new Date(currentDate.getTime() + 3000),
input: {
global: draftGlobalSlug,
},
})
await wait(4000)
await payload.jobs.run()
const retrieved = await payload.findGlobal({
slug: draftGlobalSlug,
})
expect(retrieved._status).toStrictEqual('published')
expect(retrieved.title).toStrictEqual('i will publish')
})
it('should allow global scheduled unpublish', async () => {
const draft = await payload.updateGlobal({
slug: draftGlobalSlug,
data: {
_status: 'published',
title: 'i will be a draft',
},
})
expect(draft._status).toStrictEqual('published')
const currentDate = new Date()
await payload.jobs.queue({
task: 'schedulePublish',
waitUntil: new Date(currentDate.getTime() + 3000),
input: {
type: 'unpublish',
global: draftGlobalSlug,
},
})
await wait(4000)
await payload.jobs.run()
const retrieved = await payload.findGlobal({
slug: draftGlobalSlug,
})
expect(retrieved._status).toStrictEqual('draft')
expect(retrieved.title).toStrictEqual('i will be a draft')
})
describe('server functions', () => {
let draftDoc
let event
beforeAll(async () => {
draftDoc = await payload.create({
collection: draftCollectionSlug,
data: {
title: 'my doc',
description: 'hello',
_status: 'draft',
},
})
})
it('should create using schedule-publish', async () => {
const currentDate = new Date()
const req = await createLocalReq({ user }, payload)
// use server action to create the event
await schedulePublishHandler({
req,
type: 'publish',
date: new Date(currentDate.getTime() + 3000),
doc: {
relationTo: draftCollectionSlug,
value: draftDoc.id,
},
user,
locale: 'all',
})
// fetch the job
;[event] = (
await payload.find({
collection: 'payload-jobs',
where: {
'input.doc.value': {
equals: draftDoc.id,
},
},
})
).docs
expect(event).toBeDefined()
})
it('should delete using schedule-publish', async () => {
const currentDate = new Date()
const req = await createLocalReq({ user }, payload)
// use server action to create the event
await schedulePublishHandler({
req,
type: 'publish',
date: new Date(currentDate.getTime() + 3000),
doc: {
relationTo: draftCollectionSlug,
value: draftDoc.id,
},
user,
locale: 'all',
})
// fetch the job
;[event] = (
await payload.find({
collection: 'payload-jobs',
where: {
'input.doc.value': {
equals: draftDoc.id,
},
},
})
).docs
// use server action to delete the event
await schedulePublishHandler({
req,
deleteID: event.id,
user,
})
// fetch the job
;[event] = (
await payload.find({
collection: 'payload-jobs',
where: {
'input.doc.value': {
equals: String(draftDoc.id),
},
},
})
).docs
expect(event).toBeUndefined()
})
})
})
describe('Publish Individual Locale', () => {
const collection = localizedCollectionSlug
const global = localizedGlobalSlug
describe('Collections', () => {
let postID: string
beforeEach(async () => {
await payload.delete({
collection,
where: {},
})
})
it('should save correct doc data when publishing individual locale', async () => {
// save spanish draft
const draft1 = await payload.create({
collection,
data: {
text: 'Spanish draft',
},
draft: true,
locale: 'es',
})
postID = draft1.id as any
// save english draft
const draft2 = await payload.update({
id: postID,
collection,
data: {
text: 'English draft',
description: 'My English description',
},
draft: true,
locale: 'en',
})
// save german draft
const draft3 = await payload.update({
id: postID,
collection,
data: {
text: 'German draft',
},
draft: true,
locale: 'de',
})
// publish only english
const publishedEN1 = await payload.update({
id: postID,
collection,
data: {
text: 'English published 1',
_status: 'published',
},
draft: false,
locale: 'en',
publishSpecificLocale: 'en',
})
const docWithoutSpanishDraft = await payload.findByID({
collection,
id: postID,
locale: 'all',
})
// We're getting the published version,
// which should not leak any unpublished Spanish content
// and should retain the English fields that were not explicitly
// passed in from publishedEN1
expect(docWithoutSpanishDraft.text.es).toBeUndefined()
expect(docWithoutSpanishDraft.description.en).toStrictEqual('My English description')
const docWithSpanishDraft1 = await payload.findByID({
collection,
id: postID,
locale: 'all',
draft: true,
})
// After updating English via specific locale,
// We should expect to see that Spanish translations were maintained
expect(docWithSpanishDraft1.text.es).toStrictEqual('Spanish draft')
expect(docWithSpanishDraft1.text.en).toStrictEqual('English published 1')
expect(docWithSpanishDraft1.description.en).toStrictEqual('My English description')
const publishedEN2 = await payload.update({
id: postID,
collection,
data: {
text: 'English published 2',
_status: 'published',
},
draft: false,
locale: 'en',
publishSpecificLocale: 'en',
})
const docWithoutSpanishDraft2 = await payload.findByID({
collection,
id: postID,
locale: 'all',
})
// On the second consecutive publish of a specific locale,
// Make sure we maintain draft data that has never been published
// even after two + consecutive publish events
expect(docWithoutSpanishDraft2.text.es).toBeUndefined()
expect(docWithoutSpanishDraft2.text.en).toStrictEqual('English published 2')
expect(docWithoutSpanishDraft2.description.en).toStrictEqual('My English description')
await payload.update({
id: postID,
collection,
data: {
text: 'German draft 1',
_status: 'draft',
},
draft: true,
locale: 'de',
})
const docWithGermanDraft = await payload.findByID({
collection,
id: postID,
locale: 'all',
draft: true,
})
// Make sure we retain the Spanish draft,
// which may be lost when we create a new draft with German.
// Update operation should fetch both draft locales as well as published
// and merge them.
expect(docWithGermanDraft.text.de).toStrictEqual('German draft 1')
expect(docWithGermanDraft.text.es).toStrictEqual('Spanish draft')
expect(docWithGermanDraft.text.en).toStrictEqual('English published 2')
const publishedDE = await payload.update({
id: postID,
collection,
data: {
_status: 'published',
text: 'German published 1',
},
draft: false,
locale: 'de',
publishSpecificLocale: 'de',
})
const publishedENFinal = await payload.update({
id: postID,
collection,
data: {
text: 'English published 3',
_status: 'published',
},
draft: false,
locale: 'en',
publishSpecificLocale: 'en',
})
const finalPublishedNoES = await payload.findByID({
collection,
id: postID,
locale: 'all',
})
expect(finalPublishedNoES.text.de).toStrictEqual('German published 1')
expect(finalPublishedNoES.text.en).toStrictEqual('English published 3')
expect(finalPublishedNoES.text.es).toBeUndefined()
const finalDraft = await payload.findByID({
collection,
id: postID,
locale: 'all',
draft: true,
})
expect(finalDraft.text.de).toStrictEqual('German published 1')
expect(finalDraft.text.en).toStrictEqual('English published 3')
expect(finalDraft.text.es).toStrictEqual('Spanish draft')
const published = await payload.update({
collection,
id: postID,
data: {
_status: 'published',
},
})
const finalPublished = await payload.findByID({
collection,
id: postID,
locale: 'all',
draft: true,
})
expect(finalPublished.text.de).toStrictEqual('German published 1')
expect(finalPublished.text.en).toStrictEqual('English published 3')
expect(finalPublished.text.es).toStrictEqual('Spanish draft')
})
it('should not leak draft data', async () => {
const draft = await payload.create({
collection,
data: {
text: 'Spanish draft',
},
draft: true,
locale: 'es',
})
const published = await payload.update({
id: draft.id,
collection,
data: {
text: 'English publish',
_status: 'published',
},
draft: false,
publishSpecificLocale: 'en',
})
const publishedOnlyEN = await payload.findByID({
collection,
id: draft.id,
locale: 'all',
})
expect(publishedOnlyEN.text.es).toBeUndefined()
expect(publishedOnlyEN.text.en).toStrictEqual('English publish')
})
it('should merge draft data from other locales when publishing all', async () => {
const draft = await payload.create({
collection,
data: {
text: 'Spanish draft',
},
draft: true,
locale: 'es',
})
const published = await payload.update({
id: draft.id,
collection,
data: {
text: 'English publish',
_status: 'published',
},
draft: false,
publishSpecificLocale: 'en',
})
const publishedOnlyEN = await payload.findByID({
collection,
id: draft.id,
locale: 'all',
})
expect(publishedOnlyEN.text.es).toBeUndefined()
expect(publishedOnlyEN.text.en).toStrictEqual('English publish')
const published2 = await payload.update({
id: draft.id,
collection,
data: {
_status: 'published',
},
draft: false,
})
const publishedAll = await payload.findByID({
collection,
id: published2.id,
locale: 'all',
})
expect(publishedAll.text.es).toStrictEqual('Spanish draft')
expect(publishedAll.text.en).toStrictEqual('English publish')
})
it('should publish non-default individual locale', async () => {
const draft = await payload.create({
collection,
data: {
text: 'Spanish draft',
},
draft: true,
locale: 'es',
})
const published = await payload.update({
id: draft.id,
collection,
data: {
text: 'German publish',
_status: 'published',
},
draft: false,
publishSpecificLocale: 'de',
})
const publishedOnlyDE = await payload.findByID({
collection,
id: published.id,
locale: 'all',
})
expect(publishedOnlyDE.text.es).toBeUndefined()
expect(publishedOnlyDE.text.en).toBeUndefined()
expect(publishedOnlyDE.text.de).toStrictEqual('German publish')
})
it('should show correct data in latest version', async () => {
const draft = await payload.create({
collection,
data: {
text: 'Spanish draft',
},
draft: true,
locale: 'es',
})
const published = await payload.update({
id: draft.id,
collection,
data: {
text: 'English publish',
_status: 'published',
},
draft: false,
publishSpecificLocale: 'en',
})
const publishedOnlyEN = await payload.findByID({
collection,
id: published.id,
locale: 'all',
})
expect(publishedOnlyEN.text.es).toBeUndefined()
expect(publishedOnlyEN.text.en).toStrictEqual('English publish')
const allVersions = await payload.findVersions({
collection,
locale: 'all',
})
const versions = allVersions.docs.filter(
(version) => version.parent === published.id && version.snapshot !== true,
)
const latestVersion = versions[0].version
expect(latestVersion.text.es).toBeUndefined()
expect(latestVersion.text.en).toStrictEqual('English publish')
})
})
describe('Globals', () => {
it('should save correct global data when publishing individual locale', async () => {
// publish german
await payload.updateGlobal({
slug: global,
data: {
title: 'German published',
_status: 'published',
},
locale: 'de',
})
// save spanish draft
await payload.updateGlobal({
slug: global,
data: {
title: 'Spanish draft',
content: 'Spanish draft content',
},
draft: true,
locale: 'es',
})
// publish only english
await payload.updateGlobal({
slug: global,
data: {
title: 'Eng published',
_status: 'published',
},
locale: 'en',
publishSpecificLocale: 'en',
})
const globalData = await payload.findGlobal({
slug: global,
locale: 'all',
})
// Expect only previously published data to be present
expect(globalData.title.es).toBeUndefined()
expect(globalData.title.en).toStrictEqual('Eng published')
expect(globalData.title.de).toStrictEqual('German published')
})
it('should not leak draft data', async () => {
// save spanish draft
await payload.updateGlobal({
slug: global,
data: {
title: 'Another spanish draft',
},
draft: true,
locale: 'es',
})
// publish only english
await payload.updateGlobal({
slug: global,
data: {
title: 'Eng published',
_status: 'published',
},
draft: false,
locale: 'en',
publishSpecificLocale: 'en',
})
const globalData = await payload.findGlobal({
slug: global,
locale: 'all',
})
// Expect no draft data to be present
expect(globalData.title.es).toBeUndefined()
expect(globalData.title.en).toStrictEqual('Eng published')
})
it('should merge draft data from other locales when publishing all', async () => {
// save spanish draft
await payload.updateGlobal({
slug: global,
data: {
title: 'Spanish draft',
content: 'Spanish draft content',
},
draft: true,
locale: 'es',
})
// publish only english
await payload.updateGlobal({
slug: global,
data: {
title: 'Eng published',
_status: 'published',
},
locale: 'en',
publishSpecificLocale: 'en',
})
const publishedOnlyEN = await payload.findGlobal({
slug: global,
locale: 'all',
})
expect(publishedOnlyEN.title.es).toBeUndefined()
expect(publishedOnlyEN.title.en).toStrictEqual('Eng published')
await payload.updateGlobal({
slug: global,
data: {
_status: 'published',
},
})
const publishedAll = await payload.findGlobal({
slug: global,
locale: 'all',
})
expect(publishedAll.title.es).toStrictEqual('Spanish draft')
expect(publishedAll.title.en).toStrictEqual('Eng published')
})
it('should publish non-default individual locale', async () => {
// save spanish draft
await payload.updateGlobal({
slug: global,
data: {
title: 'Test span draft',
content: 'Test span draft content',
},
draft: true,
locale: 'es',
})
// publish only german
await payload.updateGlobal({
slug: global,
data: {
title: 'German published',
_status: 'published',
},
locale: 'de',
publishSpecificLocale: 'de',
})
const globalData = await payload.findGlobal({
slug: global,
locale: 'all',
})
// Expect only published data to be present
expect(globalData.title.es).toBeFalsy()
expect(globalData.title.de).toStrictEqual('German published')
})
it('should show correct data in latest version', async () => {
// save spanish draft
await payload.updateGlobal({
slug: global,
data: {
title: 'New spanish draft',
content: 'New spanish draft content',
},
draft: true,
locale: 'es',
})
// publish only english
await payload.updateGlobal({
slug: global,
data: {
title: 'New eng',
_status: 'published',
},
draft: false,
publishSpecificLocale: 'en',
})
const allVersions = await payload.findGlobalVersions({
slug: global,
locale: 'all',
where: {
'version._status': {
equals: 'published',
},
},
})
const versions = allVersions.docs
const latestVersion = versions[0].version
expect(latestVersion.title.es).toBeFalsy()
expect(latestVersion.title.en).toStrictEqual('New eng')
})
})
})
})