Files
payloadcms/test/fields/int.spec.ts
Patrik e6f8ca6fd0 fix: deduplicate custom array id fields (#13064)
When adding a custom ID field to an array's config, both the default
field provided by Payload, and the custom ID field, exist in the
resulting config. This can lead to problems when the looking up the
field's config, where either one or the other will be returned.

Fixes #12978
2025-07-07 13:06:31 -07:00

3393 lines
90 KiB
TypeScript

import type { MongooseAdapter } from '@payloadcms/db-mongodb'
import type { IndexDirection, IndexOptions } from 'mongoose'
import path from 'path'
import { type Payload, reload } from 'payload'
import { fileURLToPath } from 'url'
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
import type { BlockField, GroupField } from './payload-types.js'
import { devUser } from '../credentials.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import { isMongoose } from '../helpers/isMongoose.js'
import { arrayDefaultValue } from './collections/Array/index.js'
import { blocksDoc } from './collections/Blocks/shared.js'
import { dateDoc } from './collections/Date/shared.js'
import { groupDefaultChild, groupDefaultValue } from './collections/Group/index.js'
import { namedGroupDoc } from './collections/Group/shared.js'
import { defaultNumber } from './collections/Number/index.js'
import { numberDoc } from './collections/Number/shared.js'
import { pointDoc } from './collections/Point/shared.js'
import {
localizedTextValue,
namedTabDefaultValue,
namedTabText,
} from './collections/Tabs/constants.js'
import { tabsDoc } from './collections/Tabs/shared.js'
import { defaultText } from './collections/Text/shared.js'
import { clearAndSeedEverything } from './seed.js'
import {
arrayFieldsSlug,
blockFieldsSlug,
checkboxFieldsSlug,
collapsibleFieldsSlug,
groupFieldsSlug,
numberFieldsSlug,
relationshipFieldsSlug,
tabsFieldsSlug,
textFieldsSlug,
} from './slugs.js'
let restClient: NextRESTClient
let user: any
let payload: Payload
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
describe('Fields', () => {
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 () => {
await payload.destroy()
})
beforeEach(async () => {
await clearAndSeedEverything(payload)
await restClient.login({
slug: 'users',
credentials: devUser,
})
user = await payload.login({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
})
describe('text', () => {
let doc
const text = 'text field'
beforeEach(async () => {
doc = await payload.create({
collection: 'text-fields',
data: { text },
})
})
it('creates with default values', () => {
expect(doc.text).toStrictEqual(text)
expect(doc.defaultString).toStrictEqual(defaultText)
expect(doc.defaultEmptyString).toStrictEqual('')
expect(doc.defaultFunction).toStrictEqual(defaultText)
expect(doc.defaultAsync).toStrictEqual(defaultText)
})
it('should populate default values in beforeValidate hook', async () => {
const { dependentOnFieldWithDefaultValue, fieldWithDefaultValue } = await payload.create({
collection: 'text-fields',
data: { text },
})
expect(fieldWithDefaultValue).toEqual(dependentOnFieldWithDefaultValue)
})
it('should populate function default values from req', async () => {
const text = await payload.create({
req: {
context: {
defaultValue: 'from-context',
},
},
collection: 'text-fields',
data: { text: 'required' },
})
expect(text.defaultValueFromReq).toBe('from-context')
})
it('should localize an array of strings using hasMany', async () => {
const localizedHasMany = ['hello', 'world']
const { id } = await payload.create({
collection: 'text-fields',
data: {
localizedHasMany,
text,
},
locale: 'en',
})
const localizedDoc = await payload.findByID({
id,
collection: 'text-fields',
locale: 'all',
})
// @ts-expect-error
expect(localizedDoc.localizedHasMany.en).toEqual(localizedHasMany)
})
it('should query hasMany in', async () => {
const hit = await payload.create({
collection: 'text-fields',
data: {
hasMany: ['one', 'five'],
text: 'required',
},
})
const miss = await payload.create({
collection: 'text-fields',
data: {
hasMany: ['two'],
text: 'required',
},
})
const { docs } = await payload.find({
collection: 'text-fields',
where: {
hasMany: {
in: ['one'],
},
},
})
const hitResult = docs.find(({ id: findID }) => hit.id === findID)
const missResult = docs.find(({ id: findID }) => miss.id === findID)
expect(hitResult).toBeDefined()
expect(missResult).toBeFalsy()
})
it('should query like on value', async () => {
const miss = await payload.create({
collection: 'text-fields',
data: {
text: 'dog',
},
})
const hit = await payload.create({
collection: 'text-fields',
data: {
text: 'cat',
},
})
const { docs } = await payload.find({
collection: 'text-fields',
where: {
text: {
like: 'cat',
},
},
})
const hitResult = docs.find(({ id: findID }) => hit.id === findID)
const missResult = docs.find(({ id: findID }) => miss.id === findID)
expect(hitResult).toBeDefined()
expect(missResult).toBeFalsy()
})
it('should query not_like on value', async () => {
const hit = await payload.create({
collection: 'text-fields',
data: {
text: 'dog',
},
})
const miss = await payload.create({
collection: 'text-fields',
data: {
text: 'cat',
},
})
const { docs } = await payload.find({
collection: 'text-fields',
where: {
text: {
not_like: 'cat',
},
},
})
const hitResult = docs.find(({ id: findID }) => hit.id === findID)
const missResult = docs.find(({ id: findID }) => miss.id === findID)
expect(hitResult).toBeDefined()
expect(missResult).toBeFalsy()
})
it('should query hasMany within an array', async () => {
const docFirst = await payload.create({
collection: 'text-fields',
data: {
text: 'required',
array: [
{
texts: ['text_1', 'text_2'],
},
],
},
})
const docSecond = await payload.create({
collection: 'text-fields',
data: {
text: 'required',
array: [
{
texts: ['text_other', 'text_2'],
},
],
},
})
const resEqualsFull = await payload.find({
collection: 'text-fields',
where: {
'array.texts': {
equals: 'text_2',
},
},
sort: '-createdAt',
})
expect(resEqualsFull.docs.find((res) => res.id === docFirst.id)).toBeDefined()
expect(resEqualsFull.docs.find((res) => res.id === docSecond.id)).toBeDefined()
expect(resEqualsFull.totalDocs).toBe(2)
const resEqualsFirst = await payload.find({
collection: 'text-fields',
where: {
'array.texts': {
equals: 'text_1',
},
},
sort: '-createdAt',
})
expect(resEqualsFirst.docs.find((res) => res.id === docFirst.id)).toBeDefined()
expect(resEqualsFirst.docs.find((res) => res.id === docSecond.id)).toBeUndefined()
expect(resEqualsFirst.totalDocs).toBe(1)
const resContainsSecond = await payload.find({
collection: 'text-fields',
where: {
'array.texts': {
contains: 'text_other',
},
},
sort: '-createdAt',
})
expect(resContainsSecond.docs.find((res) => res.id === docFirst.id)).toBeUndefined()
expect(resContainsSecond.docs.find((res) => res.id === docSecond.id)).toBeDefined()
expect(resContainsSecond.totalDocs).toBe(1)
const resInSecond = await payload.find({
collection: 'text-fields',
where: {
'array.texts': {
in: ['text_other'],
},
},
sort: '-createdAt',
})
expect(resInSecond.docs.find((res) => res.id === docFirst.id)).toBeUndefined()
expect(resInSecond.docs.find((res) => res.id === docSecond.id)).toBeDefined()
expect(resInSecond.totalDocs).toBe(1)
})
it('should query hasMany within blocks', async () => {
const docFirst = await payload.create({
collection: 'text-fields',
data: {
text: 'required',
blocks: [
{
blockType: 'blockWithText',
texts: ['text_1', 'text_2'],
},
],
},
})
const docSecond = await payload.create({
collection: 'text-fields',
data: {
text: 'required',
blocks: [
{
blockType: 'blockWithText',
texts: ['text_other', 'text_2'],
},
],
},
})
const resEqualsFull = await payload.find({
collection: 'text-fields',
where: {
'blocks.texts': {
equals: 'text_2',
},
},
sort: '-createdAt',
})
expect(resEqualsFull.docs.find((res) => res.id === docFirst.id)).toBeDefined()
expect(resEqualsFull.docs.find((res) => res.id === docSecond.id)).toBeDefined()
expect(resEqualsFull.totalDocs).toBe(2)
const resEqualsFirst = await payload.find({
collection: 'text-fields',
where: {
'blocks.texts': {
equals: 'text_1',
},
},
sort: '-createdAt',
})
expect(resEqualsFirst.docs.find((res) => res.id === docFirst.id)).toBeDefined()
expect(resEqualsFirst.docs.find((res) => res.id === docSecond.id)).toBeUndefined()
expect(resEqualsFirst.totalDocs).toBe(1)
const resContainsSecond = await payload.find({
collection: 'text-fields',
where: {
'blocks.texts': {
contains: 'text_other',
},
},
sort: '-createdAt',
})
expect(resContainsSecond.docs.find((res) => res.id === docFirst.id)).toBeUndefined()
expect(resContainsSecond.docs.find((res) => res.id === docSecond.id)).toBeDefined()
expect(resContainsSecond.totalDocs).toBe(1)
const resInSecond = await payload.find({
collection: 'text-fields',
where: {
'blocks.texts': {
in: ['text_other'],
},
},
sort: '-createdAt',
})
expect(resInSecond.docs.find((res) => res.id === docFirst.id)).toBeUndefined()
expect(resInSecond.docs.find((res) => res.id === docSecond.id)).toBeDefined()
expect(resInSecond.totalDocs).toBe(1)
})
it('should delete rows when updating hasMany with empty array', async () => {
const { id: createdDocId } = await payload.create({
collection: textFieldsSlug,
data: {
text: 'hasMany deletion test',
hasMany: ['one', 'two', 'three'],
},
})
await payload.update({
collection: textFieldsSlug,
id: createdDocId,
data: {
hasMany: [],
},
})
const resultingDoc = await payload.findByID({
collection: textFieldsSlug,
id: createdDocId,
})
expect(resultingDoc.hasMany).toHaveLength(0)
})
})
describe('relationship', () => {
let textDoc
let otherTextDoc
let selfReferencing
let parent
let child
let grandChild
let relationshipInArray
const textDocText = 'text document'
const otherTextDocText = 'alt text'
const relationshipText = 'relationship text'
beforeEach(async () => {
textDoc = await payload.create({
collection: 'text-fields',
data: {
text: textDocText,
},
})
otherTextDoc = await payload.create({
collection: 'text-fields',
data: {
text: otherTextDocText,
},
})
const relationship = { relationTo: 'text-fields', value: textDoc.id }
parent = await payload.create({
collection: relationshipFieldsSlug,
data: {
relationship,
text: relationshipText,
},
})
child = await payload.create({
collection: relationshipFieldsSlug,
data: {
relationToSelf: parent.id,
relationship,
text: relationshipText,
},
})
grandChild = await payload.create({
collection: relationshipFieldsSlug,
data: {
relationToSelf: child.id,
relationship,
text: relationshipText,
},
})
selfReferencing = await payload.create({
collection: relationshipFieldsSlug,
data: {
relationship,
text: relationshipText,
},
})
relationshipInArray = await payload.create({
collection: relationshipFieldsSlug,
data: {
array: [
{
relationship: otherTextDoc.id,
},
],
relationship,
},
})
})
it('should query parent self-reference', async () => {
const childResult = await payload.find({
collection: relationshipFieldsSlug,
where: {
relationToSelf: { equals: parent.id },
},
})
const grandChildResult = await payload.find({
collection: relationshipFieldsSlug,
where: {
relationToSelf: { equals: child.id },
},
})
const anyChildren = await payload.find({
collection: relationshipFieldsSlug,
})
const allChildren = await payload.find({
collection: relationshipFieldsSlug,
where: {
'relationToSelf.text': { equals: relationshipText },
},
})
expect(childResult.docs[0].id).toStrictEqual(child.id)
expect(grandChildResult.docs[0].id).toStrictEqual(grandChild.id)
expect(allChildren.docs).toHaveLength(2)
})
it('should query relationship inside array', async () => {
const result = await payload.find({
collection: relationshipFieldsSlug,
where: {
'array.relationship.text': { equals: otherTextDocText },
},
})
expect(result.docs).toHaveLength(1)
expect(result.docs[0]).toMatchObject(relationshipInArray)
})
it('should query text in row after relationship', async () => {
const row = await payload.create({
collection: 'row-fields',
data: { title: 'some-title', id: 'custom-row-id' },
})
const textDoc = await payload.create({
collection: 'text-fields',
data: { text: 'asd' },
})
const rel = await payload.create({
collection: 'relationship-fields',
data: {
relationship: { relationTo: 'text-fields', value: textDoc },
relationToRow: row.id,
relationToRowMany: [row.id],
},
})
const result = await payload.find({
collection: 'relationship-fields',
where: {
'relationToRow.title': { equals: 'some-title' },
'relationToRowMany.title': { equals: 'some-title' },
},
})
expect(result.docs[0].id).toBe(rel.id)
expect(result.totalDocs).toBe(1)
})
})
describe('rows', () => {
it('should show proper validation error message on text field within row field', async () => {
await expect(async () =>
payload.create({
collection: 'row-fields',
data: {
id: 'some-id',
title: '',
},
}),
).rejects.toThrow('The following field is invalid: Title within a row')
})
})
describe('timestamps', () => {
const tenMinutesAgo = new Date(Date.now() - 1000 * 60 * 10)
let doc
beforeEach(async () => {
doc = await payload.create({
collection: 'date-fields',
data: dateDoc,
})
})
it('should query updatedAt', async () => {
const { docs } = await payload.find({
collection: 'date-fields',
depth: 0,
where: {
updatedAt: {
greater_than_equal: tenMinutesAgo,
},
},
})
expect(docs.map(({ id }) => id)).toContain(doc.id)
})
it('should query createdAt', async () => {
const result = await payload.find({
collection: 'date-fields',
depth: 0,
where: {
createdAt: {
greater_than_equal: tenMinutesAgo,
},
},
})
expect(result.docs[0].id).toEqual(doc.id)
})
// Function to generate random date between start and end dates
function getRandomDate(start: Date, end: Date): string {
const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()))
return date.toISOString()
}
// Generate sample data
const dataSample = Array.from({ length: 100 }, (_, index) => {
const startDate = new Date('2024-01-01')
const endDate = new Date('2025-12-31')
return {
array: Array.from({ length: 5 }, (_, listIndex) => {
return {
date: getRandomDate(startDate, endDate),
}
}),
...dateDoc,
}
})
it('should query a date field inside an array field', async () => {
await payload.delete({ collection: 'date-fields', where: {} })
for (const doc of dataSample) {
await payload.create({
collection: 'date-fields',
data: doc,
})
}
const res = await payload.find({
collection: 'date-fields',
where: { 'array.date': { greater_than: new Date('2025-06-01').toISOString() } },
})
const filter = (doc: any) =>
doc.array.some((item) => new Date(item.date).getTime() > new Date('2025-06-01').getTime())
expect(res.docs.every(filter)).toBe(true)
expect(dataSample.filter(filter)).toHaveLength(res.totalDocs)
// eslint-disable-next-line jest/no-conditional-in-test
if (res.totalDocs > 10) {
// This is where postgres might fail! selectDistinct actually removed some rows here, because it distincts by:
// not only ID, but also created_at, updated_at, items_date
expect(res.docs).toHaveLength(10)
} else {
expect(res.docs.length).toBeLessThanOrEqual(res.totalDocs)
}
})
})
describe('select', () => {
let doc
beforeEach(async () => {
const { id } = await payload.create({
collection: 'select-fields',
data: {
selectHasManyLocalized: ['one', 'two'],
},
locale: 'en',
})
doc = await payload.findByID({
id,
collection: 'select-fields',
locale: 'all',
})
})
it('creates with hasMany localized', () => {
expect(doc.selectHasManyLocalized.en).toEqual(['one', 'two'])
})
it('retains hasMany updates', async () => {
const { id } = await payload.create({
collection: 'select-fields',
data: {
selectHasMany: ['one', 'two'],
},
})
const updatedDoc = await payload.update({
id,
collection: 'select-fields',
data: {
select: 'one',
},
})
expect(Array.isArray(updatedDoc.selectHasMany)).toBe(true)
expect(updatedDoc.selectHasMany).toEqual(['one', 'two'])
})
it('should clear select hasMany field', async () => {
const { id } = await payload.create({
collection: 'select-fields',
data: {
selectHasMany: ['one', 'two'],
},
})
const updatedDoc = await payload.update({
id,
collection: 'select-fields',
data: {
selectHasMany: [],
},
})
expect(updatedDoc.selectHasMany).toHaveLength(0)
})
it('should query hasMany in', async () => {
const hit = await payload.create({
collection: 'select-fields',
data: {
selectHasMany: ['one', 'four'],
},
})
const miss = await payload.create({
collection: 'select-fields',
data: {
selectHasMany: ['three'],
},
})
const { docs } = await payload.find({
collection: 'select-fields',
where: {
selectHasMany: {
in: ['one'],
},
},
})
const hitResult = docs.find(({ id: findID }) => hit.id === findID)
const missResult = docs.find(({ id: findID }) => miss.id === findID)
expect(hitResult).toBeDefined()
expect(missResult).toBeFalsy()
})
it('should CRUD within array hasMany', async () => {
const doc = await payload.create({
collection: 'select-fields',
data: { array: [{ selectHasMany: ['one', 'two'] }] },
})
expect(doc.array[0].selectHasMany).toStrictEqual(['one', 'two'])
const upd = await payload.update({
collection: 'select-fields',
id: doc.id,
data: {
array: [
{
id: doc.array[0].id,
selectHasMany: ['six'],
},
],
},
})
expect(upd.array[0].selectHasMany).toStrictEqual(['six'])
})
it('should CRUD within array + group hasMany', async () => {
const doc = await payload.create({
collection: 'select-fields',
data: { array: [{ group: { selectHasMany: ['one', 'two'] } }] },
})
expect(doc.array[0].group.selectHasMany).toStrictEqual(['one', 'two'])
const upd = await payload.update({
collection: 'select-fields',
id: doc.id,
data: {
array: [
{
id: doc.array[0].id,
group: { selectHasMany: ['six'] },
},
],
},
})
expect(upd.array[0].group.selectHasMany).toStrictEqual(['six'])
})
it('should work with versions', async () => {
const base = await payload.create({
collection: 'select-versions-fields',
data: { hasMany: ['a', 'b'] },
})
expect(base.hasMany).toStrictEqual(['a', 'b'])
const array = await payload.create({
collection: 'select-versions-fields',
data: { array: [{ hasManyArr: ['a', 'b'] }] },
draft: true,
})
expect(array.array[0]?.hasManyArr).toStrictEqual(['a', 'b'])
const block = await payload.create({
collection: 'select-versions-fields',
data: { blocks: [{ blockType: 'block', hasManyBlocks: ['a', 'b'] }] },
})
expect(block.blocks[0]?.hasManyBlocks).toStrictEqual(['a', 'b'])
})
it('should work with autosave', async () => {
let data = await payload.create({
collection: 'select-versions-fields',
data: { hasMany: ['a', 'b', 'c'] },
})
expect(data.hasMany).toStrictEqual(['a', 'b', 'c'])
data = await payload.update({
id: data.id,
collection: 'select-versions-fields',
data: { hasMany: ['a'] },
draft: true,
})
expect(data.hasMany).toStrictEqual(['a'])
data = await payload.update({
id: data.id,
collection: 'select-versions-fields',
data: { hasMany: ['a', 'b', 'c', 'd'] },
draft: true,
autosave: true,
})
expect(data.hasMany).toStrictEqual(['a', 'b', 'c', 'd'])
data = await payload.update({
id: data.id,
collection: 'select-versions-fields',
data: { hasMany: ['a'] },
draft: true,
autosave: true,
})
expect(data.hasMany).toStrictEqual(['a'])
})
it('should prevent against saving a value excluded by `filterOptions`', async () => {
try {
const result = await payload.create({
collection: 'select-fields',
data: {
disallowOption1: true,
selectWithFilteredOptions: 'one',
},
})
expect(result).toBeFalsy()
} catch (error) {
expect((error as Error).message).toBe(
'The following field is invalid: Select with filtered options',
)
}
const result = await payload.create({
collection: 'select-fields',
data: {
disallowOption1: true,
selectWithFilteredOptions: 'two',
},
})
expect(result).toBeTruthy()
})
})
describe('number', () => {
let doc
beforeEach(async () => {
doc = await payload.create({
collection: 'number-fields',
data: numberDoc,
})
})
it('creates with default values', () => {
expect(doc.number).toEqual(numberDoc.number)
expect(doc.min).toEqual(numberDoc.min)
expect(doc.max).toEqual(numberDoc.max)
expect(doc.positiveNumber).toEqual(numberDoc.positiveNumber)
expect(doc.negativeNumber).toEqual(numberDoc.negativeNumber)
expect(doc.decimalMin).toEqual(numberDoc.decimalMin)
expect(doc.decimalMax).toEqual(numberDoc.decimalMax)
expect(doc.defaultNumber).toEqual(defaultNumber)
})
it('should not create number below minimum', async () => {
await expect(async () =>
payload.create({
collection: 'number-fields',
data: {
min: 5,
},
}),
).rejects.toThrow('The following field is invalid: Min')
})
it('should not create number above max', async () => {
await expect(async () =>
payload.create({
collection: 'number-fields',
data: {
max: 15,
},
}),
).rejects.toThrow('The following field is invalid: Max')
})
it('should not create number below 0', async () => {
await expect(async () =>
payload.create({
collection: 'number-fields',
data: {
positiveNumber: -5,
},
}),
).rejects.toThrow('The following field is invalid: Positive Number')
})
it('should not create number above 0', async () => {
await expect(async () =>
payload.create({
collection: 'number-fields',
data: {
negativeNumber: 5,
},
}),
).rejects.toThrow('The following field is invalid: Negative Number')
})
it('should not create a decimal number below min', async () => {
await expect(async () =>
payload.create({
collection: 'number-fields',
data: {
decimalMin: -0.25,
},
}),
).rejects.toThrow('The following field is invalid: Decimal Min')
})
it('should not create a decimal number above max', async () => {
await expect(async () =>
payload.create({
collection: 'number-fields',
data: {
decimalMax: 1.5,
},
}),
).rejects.toThrow('The following field is invalid: Decimal Max')
})
it('should localize an array of numbers using hasMany', async () => {
const localizedHasMany = [5, 10]
const { id } = await payload.create({
collection: 'number-fields',
data: {
localizedHasMany,
},
locale: 'en',
})
const localizedDoc = await payload.findByID({
id,
collection: 'number-fields',
locale: 'all',
})
// @ts-expect-error
expect(localizedDoc.localizedHasMany.en).toEqual(localizedHasMany)
})
it('should query hasMany in', async () => {
const hit = await payload.create({
collection: 'number-fields',
data: {
hasMany: [5, 10],
},
})
const miss = await payload.create({
collection: 'number-fields',
data: {
hasMany: [13],
},
})
const { docs } = await payload.find({
collection: 'number-fields',
where: {
hasMany: {
in: [5],
},
},
})
const hitResult = docs.find(({ id: findID }) => hit.id === findID)
const missResult = docs.find(({ id: findID }) => miss.id === findID)
expect(hitResult).toBeDefined()
expect(missResult).toBeFalsy()
})
it('should properly query numbers with exists operator', async () => {
await payload.create({
collection: 'number-fields',
data: {
number: null,
},
})
const numbersExist = await payload.find({
collection: 'number-fields',
where: {
number: {
exists: true,
},
},
})
expect(numbersExist.totalDocs).toBe(4)
const numbersNotExists = await payload.find({
collection: 'number-fields',
where: {
number: {
exists: false,
},
},
})
expect(numbersNotExists.docs).toHaveLength(1)
})
it('should delete rows when updating hasMany with empty array', async () => {
const { id: createdDocId } = await payload.create({
collection: numberFieldsSlug,
data: {
localizedHasMany: [1, 2, 3],
},
})
await payload.update({
collection: numberFieldsSlug,
id: createdDocId,
data: {
localizedHasMany: [],
},
})
const resultingDoc = await payload.findByID({
collection: numberFieldsSlug,
id: createdDocId,
})
expect(resultingDoc.localizedHasMany).toHaveLength(0)
})
})
it('should query hasMany within an array', async () => {
const docFirst = await payload.create({
collection: 'number-fields',
data: {
array: [
{
numbers: [10, 30],
},
],
},
})
const docSecond = await payload.create({
collection: 'number-fields',
data: {
array: [
{
numbers: [10, 40],
},
],
},
})
const resEqualsFull = await payload.find({
collection: 'number-fields',
where: {
'array.numbers': {
equals: 10,
},
},
})
expect(resEqualsFull.docs.find((res) => res.id === docFirst.id)).toBeDefined()
expect(resEqualsFull.docs.find((res) => res.id === docSecond.id)).toBeDefined()
expect(resEqualsFull.totalDocs).toBe(2)
const resEqualsFirst = await payload.find({
collection: 'number-fields',
where: {
'array.numbers': {
equals: 30,
},
},
})
expect(resEqualsFirst.docs.find((res) => res.id === docFirst.id)).toBeDefined()
expect(resEqualsFirst.docs.find((res) => res.id === docSecond.id)).toBeUndefined()
expect(resEqualsFirst.totalDocs).toBe(1)
const resInSecond = await payload.find({
collection: 'number-fields',
where: {
'array.numbers': {
in: [40],
},
},
})
expect(resInSecond.docs.find((res) => res.id === docFirst.id)).toBeUndefined()
expect(resInSecond.docs.find((res) => res.id === docSecond.id)).toBeDefined()
expect(resInSecond.totalDocs).toBe(1)
})
it('should query hasMany within blocks', async () => {
const docFirst = await payload.create({
collection: 'number-fields',
data: {
blocks: [
{
blockType: 'blockWithNumber',
numbers: [10, 30],
},
],
},
})
const docSecond = await payload.create({
collection: 'number-fields',
data: {
blocks: [
{
blockType: 'blockWithNumber',
numbers: [10, 40],
},
],
},
})
const resEqualsFull = await payload.find({
collection: 'number-fields',
where: {
'blocks.numbers': {
equals: 10,
},
},
})
expect(resEqualsFull.docs.find((res) => res.id === docFirst.id)).toBeDefined()
expect(resEqualsFull.docs.find((res) => res.id === docSecond.id)).toBeDefined()
expect(resEqualsFull.totalDocs).toBe(2)
const resEqualsFirst = await payload.find({
collection: 'number-fields',
where: {
'blocks.numbers': {
equals: 30,
},
},
})
expect(resEqualsFirst.docs.find((res) => res.id === docFirst.id)).toBeDefined()
expect(resEqualsFirst.docs.find((res) => res.id === docSecond.id)).toBeUndefined()
expect(resEqualsFirst.totalDocs).toBe(1)
const resInSecond = await payload.find({
collection: 'number-fields',
where: {
'blocks.numbers': {
in: [40],
},
},
})
expect(resInSecond.docs.find((res) => res.id === docFirst.id)).toBeUndefined()
expect(resInSecond.docs.find((res) => res.id === docSecond.id)).toBeDefined()
expect(resInSecond.totalDocs).toBe(1)
})
if (isMongoose(payload)) {
describe('indexes', () => {
let indexes
const definitions: Record<string, IndexDirection> = {}
const options: Record<string, IndexOptions> = {}
beforeAll(() => {
indexes = (payload.db as MongooseAdapter).collections[
'indexed-fields'
].schema.indexes() as [Record<string, IndexDirection>, IndexOptions]
indexes.forEach((index) => {
const field = Object.keys(index[0])[0]
definitions[field] = index[0][field]
options[field] = index[1]
})
})
it('should have indexes', () => {
expect(definitions.text).toEqual(1)
})
it('should have unique sparse indexes when field is not required', () => {
expect(definitions.uniqueText).toEqual(1)
expect(options.uniqueText).toMatchObject({ sparse: true, unique: true })
})
it('should have unique indexes that are not sparse when field is required', () => {
expect(definitions.uniqueRequiredText).toEqual(1)
expect(options.uniqueText).toMatchObject({ unique: true })
})
it('should have 2dsphere indexes on point fields', () => {
expect(definitions.point).toEqual('2dsphere')
})
it('should have 2dsphere indexes on point fields in groups', () => {
expect(definitions['group.point']).toEqual('2dsphere')
})
it('should have a sparse index on a unique localized field in a group', () => {
expect(definitions['group.localizedUnique.en']).toEqual(1)
expect(options['group.localizedUnique.en']).toMatchObject({ sparse: true, unique: true })
expect(definitions['group.localizedUnique.es']).toEqual(1)
expect(options['group.localizedUnique.es']).toMatchObject({ sparse: true, unique: true })
})
it('should have unique indexes in a collapsible', () => {
expect(definitions['collapsibleLocalizedUnique.en']).toEqual(1)
expect(options['collapsibleLocalizedUnique.en']).toMatchObject({
sparse: true,
unique: true,
})
expect(definitions.collapsibleTextUnique).toEqual(1)
expect(options.collapsibleTextUnique).toMatchObject({ unique: true })
})
})
describe('version indexes', () => {
let indexes
const definitions: Record<string, IndexDirection> = {}
const options: Record<string, IndexOptions> = {}
beforeEach(() => {
indexes = (payload.db as MongooseAdapter).versions['indexed-fields'].schema.indexes() as [
Record<string, IndexDirection>,
IndexOptions,
]
indexes.forEach((index) => {
const field = Object.keys(index[0])[0]
definitions[field] = index[0][field]
options[field] = index[1]
})
})
it('should have versions indexes', () => {
expect(definitions['version.text']).toEqual(1)
})
})
}
describe('point', () => {
let doc
const point = [7, -7]
const localized = [5, -2]
const group = { point: [1, 9] }
beforeEach(async () => {
const findDoc = await payload.find({
collection: 'point-fields',
pagination: false,
})
;[doc] = findDoc.docs
})
it('should read', async () => {
if (payload.db.name === 'sqlite') {
return
}
const find = await payload.find({
collection: 'point-fields',
pagination: false,
})
;[doc] = find.docs
expect(doc.point).toEqual(pointDoc.point)
expect(doc.localized).toEqual(pointDoc.localized)
expect(doc.group).toMatchObject(pointDoc.group)
})
it('should create', async () => {
if (payload.db.name === 'sqlite') {
return
}
doc = await payload.create({
collection: 'point-fields',
data: {
group,
localized,
point,
},
})
expect(doc.point).toEqual(point)
expect(doc.localized).toEqual(localized)
expect(doc.group).toMatchObject(group)
})
it('should not create duplicate point when unique', async () => {
if (payload.db.name === 'sqlite') {
return
}
// first create the point field
doc = await payload.create({
collection: 'point-fields',
data: {
group,
localized,
point,
},
})
// Now make sure we can't create a duplicate (since 'localized' is a unique field)
await expect(() =>
payload.create({
collection: 'point-fields',
data: {
group,
localized,
point,
},
}),
).rejects.toThrow(Error)
await expect(async () =>
payload.create({
collection: 'number-fields',
data: {
min: 5,
},
}),
).rejects.toThrow('The following field is invalid: Min')
expect(doc.point).toEqual(point)
expect(doc.localized).toEqual(localized)
expect(doc.group).toMatchObject(group)
})
it('should throw validation error when "required" field is set to null', async () => {
if (payload.db.name === 'sqlite') {
return
}
// first create the point field
doc = await payload.create({
collection: 'point-fields',
data: {
localized,
point,
},
})
// try to update the required field to null
await expect(() =>
payload.update({
collection: 'point-fields',
data: {
point: null,
},
id: doc.id,
}),
).rejects.toThrow('The following field is invalid: Location')
})
it('should not throw validation error when non-"required" field is set to null', async () => {
if (payload.db.name === 'sqlite') {
return
}
// first create the point field
doc = await payload.create({
collection: 'point-fields',
data: {
localized,
point,
},
})
expect(doc.localized).toEqual(localized)
// try to update the non-required field to null
const updatedDoc = await payload.update({
collection: 'point-fields',
data: {
localized: null,
},
id: doc.id,
})
expect(updatedDoc.localized).toEqual(undefined)
})
})
describe('checkbox', () => {
beforeEach(async () => {
await payload.delete({
collection: checkboxFieldsSlug,
where: {
id: {
exists: true,
},
},
})
})
it('should query checkbox fields with exists operator', async () => {
const existsTrueDoc = await payload.create({
collection: checkboxFieldsSlug,
data: {
checkbox: true,
checkboxNotRequired: false,
},
})
const existsFalseDoc = await payload.create({
collection: checkboxFieldsSlug,
data: {
checkbox: true,
},
})
const existsFalse = await payload.find({
collection: checkboxFieldsSlug,
where: {
checkboxNotRequired: {
exists: false,
},
},
})
expect(existsFalse.totalDocs).toBe(1)
expect(existsFalse.docs[0]?.id).toEqual(existsFalseDoc.id)
const existsTrue = await payload.find({
collection: checkboxFieldsSlug,
where: {
checkboxNotRequired: {
exists: true,
},
},
})
expect(existsTrue.totalDocs).toBe(1)
expect(existsTrue.docs[0]?.id).toEqual(existsTrueDoc.id)
})
})
describe('unique indexes', () => {
it('should throw validation error saving on unique fields', async () => {
const data = {
text: 'a',
uniqueText: 'a',
}
await payload.create({
collection: 'indexed-fields',
data,
})
expect(async () => {
const result = await payload.create({
collection: 'indexed-fields',
data,
})
return result.error
}).toBeDefined()
})
it('should throw validation error saving on unique relationship fields hasMany: false non polymorphic', async () => {
const textDoc = await payload.create({ collection: 'text-fields', data: { text: 'asd' } })
await payload
.create({
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: '1',
text: '2',
uniqueRequiredText: '3',
uniqueRelationship: textDoc.id,
},
})
// Skip mongodb unique error because it threats localizedUniqueRequriedText.es as undefined
.then((doc) =>
payload.update({
locale: 'es',
collection: 'indexed-fields',
data: { localizedUniqueRequiredText: '20' },
id: doc.id,
}),
)
await expect(
payload.create({
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: '4',
text: '5',
uniqueRequiredText: '10',
uniqueRelationship: textDoc.id,
},
}),
).rejects.toBeTruthy()
})
it('should throw validation error saving on unique relationship fields hasMany: true', async () => {
const textDoc = await payload.create({ collection: 'text-fields', data: { text: 'asd' } })
await payload
.create({
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: '1',
text: '2',
uniqueRequiredText: '3',
uniqueHasManyRelationship: [textDoc.id],
},
})
// Skip mongodb unique error because it threats localizedUniqueRequriedText.es as undefined
.then((doc) =>
payload.update({
locale: 'es',
collection: 'indexed-fields',
data: { localizedUniqueRequiredText: '40' },
id: doc.id,
}),
)
// Should allow the same relationship on a diferrent field!
await payload
.create({
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: '31',
text: '24',
uniqueRequiredText: '55',
uniqueHasManyRelationship_2: [textDoc.id],
},
})
// Skip mongodb unique error because it threats localizedUniqueRequriedText.es as undefined
.then((doc) =>
payload.update({
locale: 'es',
collection: 'indexed-fields',
data: { localizedUniqueRequiredText: '30' },
id: doc.id,
}),
)
await expect(
payload.create({
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: '4',
text: '5',
uniqueRequiredText: '10',
uniqueHasManyRelationship: [textDoc.id],
},
}),
).rejects.toBeTruthy()
})
it('should throw validation error saving on unique relationship fields polymorphic not hasMany', async () => {
const textDoc = await payload.create({ collection: 'text-fields', data: { text: 'asd' } })
await payload
.create({
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: '1',
text: '2',
uniqueRequiredText: '3',
uniquePolymorphicRelationship: { relationTo: 'text-fields', value: textDoc.id },
},
})
// Skip mongodb unique error because it threats localizedUniqueRequriedText.es as undefined
.then((doc) =>
payload.update({
locale: 'es',
collection: 'indexed-fields',
data: { localizedUniqueRequiredText: '20' },
id: doc.id,
}),
)
// Should allow the same relationship on a diferrent field!
await payload
.create({
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: '31',
text: '24',
uniqueRequiredText: '55',
uniquePolymorphicRelationship_2: { relationTo: 'text-fields', value: textDoc.id },
},
})
// Skip mongodb unique error because it threats localizedUniqueRequriedText.es as undefined
.then((doc) =>
payload.update({
locale: 'es',
collection: 'indexed-fields',
data: { localizedUniqueRequiredText: '100' },
id: doc.id,
}),
)
await expect(
payload.create({
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: '4',
text: '5',
uniqueRequiredText: '10',
uniquePolymorphicRelationship: { relationTo: 'text-fields', value: textDoc.id },
},
}),
).rejects.toBeTruthy()
})
it('should throw validation error saving on unique relationship fields polymorphic hasMany: true', async () => {
const textDoc = await payload.create({ collection: 'text-fields', data: { text: 'asd' } })
await payload
.create({
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: '1',
text: '2',
uniqueRequiredText: '3',
uniqueHasManyPolymorphicRelationship: [
{ relationTo: 'text-fields', value: textDoc.id },
],
},
})
.then((doc) =>
payload.update({
locale: 'es',
collection: 'indexed-fields',
data: { localizedUniqueRequiredText: '100' },
id: doc.id,
}),
)
// Should allow the same relationship on a diferrent field!
await payload
.create({
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: '31',
text: '24',
uniqueRequiredText: '55',
uniqueHasManyPolymorphicRelationship_2: [
{ relationTo: 'text-fields', value: textDoc.id },
],
},
})
// Skip mongodb unique error because it threats localizedUniqueRequriedText.es as undefined
.then((doc) =>
payload.update({
locale: 'es',
collection: 'indexed-fields',
data: { localizedUniqueRequiredText: '300' },
id: doc.id,
}),
)
await expect(
payload.create({
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: '4',
text: '5',
uniqueRequiredText: '10',
uniqueHasManyPolymorphicRelationship: [
{ relationTo: 'text-fields', value: textDoc.id },
],
},
}),
).rejects.toBeTruthy()
})
it('should not throw validation error saving multiple null values for unique fields', async () => {
const data = {
localizedUniqueRequiredText: 'en1',
text: 'a',
uniqueRequiredText: 'a',
// uniqueText omitted on purpose
}
const doc = await payload.create({
collection: 'indexed-fields',
data,
})
// Update spanish so we do not run into the unique constraint for other locales
await payload.update({
id: doc.id,
collection: 'indexed-fields',
data: {
localizedUniqueRequiredText: 'es1',
},
locale: 'es',
})
data.uniqueRequiredText = 'b'
const result = await payload.create({
collection: 'indexed-fields',
data: { ...data, localizedUniqueRequiredText: 'en2' },
})
expect(result.id).toBeDefined()
})
it('should duplicate with unique fields', async () => {
const data = {
text: 'a',
// uniqueRequiredText: 'duplicate',
}
const doc = await payload.create({
collection: 'indexed-fields',
data,
})
const result = await payload.duplicate({
id: doc.id,
collection: 'indexed-fields',
})
expect(result.id).not.toEqual(doc.id)
expect(result.uniqueRequiredText).toStrictEqual('uniqueRequired - Copy')
})
})
describe('array', () => {
let doc
const collection = arrayFieldsSlug
beforeEach(async () => {
doc = await payload.create({
collection,
data: {},
})
})
it('should create with ids and nested ids', async () => {
const docWithIDs = (await payload.create({
collection: groupFieldsSlug,
data: namedGroupDoc,
})) as Partial<GroupField>
expect(docWithIDs.group.subGroup.arrayWithinGroup[0].id).toBeDefined()
})
it('should create with defaultValue', () => {
expect(doc.items).toMatchObject(arrayDefaultValue)
expect(doc.localized).toMatchObject(arrayDefaultValue)
})
it('should create and update localized subfields with versions', async () => {
const doc = await payload.create({
collection,
data: {
items: [
{
localizedText: 'test',
text: 'required',
},
],
localized: [
{
text: 'english',
},
],
},
})
const spanish = await payload.update({
id: doc.id,
collection,
data: {
items: [
{
id: doc.items[0].id,
localizedText: 'spanish',
text: 'required',
},
],
},
locale: 'es',
})
const result = await payload.findByID({
id: doc.id,
collection,
locale: 'all',
})
expect(doc.items[0].localizedText).toStrictEqual('test')
expect(spanish.items[0].localizedText).toStrictEqual('spanish')
expect(result.items[0].localizedText.en).toStrictEqual('test')
expect(result.items[0].localizedText.es).toStrictEqual('spanish')
})
it('should create and append localized items to nested array with versions', async () => {
const doc = await payload.create({
collection,
data: {
items: [{ text: 'req' }],
localized: [{ text: 'req' }],
nestedArrayLocalized: [
{
array: [
{
text: 'marcelo',
},
],
},
],
},
})
const res = await payload.update({
id: doc.id,
collection,
data: {
nestedArrayLocalized: [
...doc.nestedArrayLocalized,
{
array: [
{
text: 'alejandro',
},
{
text: 'raul',
},
],
},
{
array: [
{
text: 'amigo',
},
],
},
],
},
})
expect(res.nestedArrayLocalized).toHaveLength(3)
expect(res.nestedArrayLocalized[0].array[0].text).toBe('marcelo')
expect(res.nestedArrayLocalized[1].array[0].text).toBe('alejandro')
expect(res.nestedArrayLocalized[1].array[1].text).toBe('raul')
expect(res.nestedArrayLocalized[2].array[0].text).toBe('amigo')
})
it('should create with nested array', async () => {
const subArrayText = 'something expected'
const doc = await payload.create({
collection,
data: {
items: [
{
subArray: [
{
text: subArrayText,
},
],
text: 'test',
},
],
},
})
const result = await payload.findByID({
id: doc.id,
collection,
})
expect(result.items[0]).toMatchObject({
subArray: [
{
text: subArrayText,
},
],
text: 'test',
})
expect(result.items[0].subArray[0].text).toStrictEqual(subArrayText)
})
it('should update without overwriting other locales with defaultValue', async () => {
const localized = [{ text: 'unique' }]
const enText = 'english'
const esText = 'spanish'
const { id } = await payload.create({
collection,
data: {
localized,
},
})
const enDoc = await payload.update({
id,
collection,
data: {
localized: [{ text: enText }],
},
locale: 'en',
})
const esDoc = await payload.update({
id,
collection,
data: {
localized: [{ text: esText }],
},
locale: 'es',
})
const allLocales = (await payload.findByID({
id,
collection,
locale: 'all',
})) as unknown as {
localized: {
en: unknown
es: unknown
}
}
expect(enDoc.localized[0].text).toStrictEqual(enText)
expect(esDoc.localized[0].text).toStrictEqual(esText)
expect(allLocales.localized.en[0].text).toStrictEqual(enText)
expect(allLocales.localized.es[0].text).toStrictEqual(esText)
})
it('should query by the same array', async () => {
const doc = await payload.create({
collection,
data: {
items: [
{
localizedText: 'test',
text: 'required',
anotherText: 'another',
},
],
localized: [{ text: 'a' }],
},
})
// left join collection_items + left join collection_items_locales
const {
docs: [res],
} = await payload.find({
collection,
where: {
and: [
{
'items.localizedText': {
equals: 'test',
},
},
{
'items.anotherText': {
equals: 'another',
},
},
{
'items.text': {
equals: 'required',
},
},
],
},
})
expect(res.id).toBe(doc.id)
})
it('should show proper validation error on text field in nested array', async () => {
await expect(async () =>
payload.create({
collection,
data: {
items: [
{
text: 'required',
subArray: [
{
textTwo: '',
},
],
},
],
},
}),
).rejects.toThrow('The following field is invalid: Items 1 > Sub Array 1 > Second text field')
})
it('should show proper validation error on text field in row field in nested array', async () => {
await expect(async () =>
payload.create({
collection,
data: {
items: [
{
text: 'required',
subArray: [
{
textInRow: '',
},
],
},
],
},
}),
).rejects.toThrow('The following field is invalid: Items 1 > Sub Array 1 > Text In Row')
})
it('should not have multiple instances of the id field in an array with a nested custom id field', () => {
const arraysCollection = payload.config.collections.find(
(collection) => collection.slug === arrayFieldsSlug,
)
const arrayWithNestedCustomIDField = arraysCollection?.fields.find(
(f) => f.name === 'arrayWithCustomID',
)
const idFields = arrayWithNestedCustomIDField?.fields.filter((f) => f.name === 'id')
expect(idFields).toHaveLength(1)
expect(idFields[0].admin?.disableListFilter).toBe(true)
})
})
describe('group', () => {
let document
beforeEach(async () => {
document = await payload.create({
collection: groupFieldsSlug,
data: {},
})
})
it('should create with defaultValue', () => {
expect(document.group.defaultParent).toStrictEqual(groupDefaultValue)
expect(document.group.defaultChild).toStrictEqual(groupDefaultChild)
})
it('should not have duplicate keys', () => {
expect(document.arrayOfGroups[0]).toMatchObject({
id: expect.any(String),
groupItem: {
text: 'Hello world',
},
})
})
it('should work with unnamed group', async () => {
const groupDoc = await payload.create({
collection: groupFieldsSlug,
data: {
insideUnnamedGroup: 'Hello world',
deeplyNestedGroup: { insideNestedUnnamedGroup: 'Secondfield' },
},
})
expect(groupDoc).toMatchObject({
id: expect.anything(),
insideUnnamedGroup: 'Hello world',
deeplyNestedGroup: {
insideNestedUnnamedGroup: 'Secondfield',
},
})
})
it('should work with unnamed group - graphql', async () => {
const mutation = `mutation {
createGroupField(
data: {
insideUnnamedGroup: "Hello world",
deeplyNestedGroup: { insideNestedUnnamedGroup: "Secondfield" },
group: {text: "hello"}
}
) {
insideUnnamedGroup
deeplyNestedGroup {
insideNestedUnnamedGroup
}
}
}`
const groupDoc = await restClient.GRAPHQL_POST({
body: JSON.stringify({ query: mutation }),
})
const data = (await groupDoc.json()).data.createGroupField
expect(data).toMatchObject({
insideUnnamedGroup: 'Hello world',
deeplyNestedGroup: {
insideNestedUnnamedGroup: 'Secondfield',
},
})
})
it('should query a subfield within a localized group', async () => {
const text = 'find this'
const hit = await payload.create({
collection: groupFieldsSlug,
data: {
localizedGroup: {
text,
},
},
})
const miss = await payload.create({
collection: groupFieldsSlug,
data: {
localizedGroup: {
text: 'do not find this',
},
},
})
const result = await payload.find({
collection: groupFieldsSlug,
where: {
'localizedGroup.text': { equals: text },
},
})
const resultIDs = result.docs.map(({ id }) => id)
expect(resultIDs).toContain(hit.id)
expect(resultIDs).not.toContain(miss.id)
})
it('should insert/read camelCase group with nested arrays + localized', async () => {
const res = await payload.create({
collection: 'group-fields',
data: {
group: { text: 'required' },
camelCaseGroup: {
array: [
{
text: 'text',
array: [
{
text: 'nested',
},
],
},
],
},
},
})
expect(res.camelCaseGroup.array[0].text).toBe('text')
expect(res.camelCaseGroup.array[0].array[0].text).toBe('nested')
})
it('should insert/update/read localized group with array inside', async () => {
const doc = await payload.create({
collection: 'group-fields',
locale: 'en',
data: {
group: { text: 'req' },
localizedGroupArr: {
array: [{ text: 'text-en' }],
},
},
})
expect(doc.localizedGroupArr.array[0].text).toBe('text-en')
const esDoc = await payload.update({
collection: 'group-fields',
locale: 'es',
id: doc.id,
data: {
localizedGroupArr: {
array: [{ text: 'text-es' }],
},
},
})
expect(esDoc.localizedGroupArr.array[0].text).toBe('text-es')
const allDoc = await payload.findByID({
collection: 'group-fields',
id: doc.id,
locale: 'all',
})
expect(allDoc.localizedGroupArr.en.array[0].text).toBe('text-en')
expect(allDoc.localizedGroupArr.es.array[0].text).toBe('text-es')
})
it('should insert/update/read localized group with select hasMany inside', async () => {
const doc = await payload.create({
collection: 'group-fields',
locale: 'en',
data: {
group: { text: 'req' },
localizedGroupSelect: {
select: ['one', 'two'],
},
},
})
expect(doc.localizedGroupSelect.select).toStrictEqual(['one', 'two'])
const esDoc = await payload.update({
collection: 'group-fields',
locale: 'es',
id: doc.id,
data: {
localizedGroupSelect: {
select: ['one'],
},
},
})
expect(esDoc.localizedGroupSelect.select).toStrictEqual(['one'])
const allDoc = await payload.findByID({
collection: 'group-fields',
id: doc.id,
locale: 'all',
})
expect(allDoc.localizedGroupSelect.en.select).toStrictEqual(['one', 'two'])
expect(allDoc.localizedGroupSelect.es.select).toStrictEqual(['one'])
})
it('should insert/update/read localized group with relationship inside', async () => {
const rel_1 = await payload.create({
collection: 'email-fields',
data: { email: 'pro123@gmail.com' },
})
const rel_2 = await payload.create({
collection: 'email-fields',
data: { email: 'frank@gmail.com' },
})
const doc = await payload.create({
collection: 'group-fields',
depth: 0,
data: {
group: { text: 'requireddd' },
localizedGroupRel: {
email: rel_1.id,
},
},
})
expect(doc.localizedGroupRel.email).toBe(rel_1.id)
const upd = await payload.update({
collection: 'group-fields',
depth: 0,
id: doc.id,
locale: 'es',
data: {
localizedGroupRel: {
email: rel_2.id,
},
},
})
expect(upd.localizedGroupRel.email).toBe(rel_2.id)
const docAll = await payload.findByID({
collection: 'group-fields',
id: doc.id,
locale: 'all',
depth: 0,
})
expect(docAll.localizedGroupRel.en.email).toBe(rel_1.id)
expect(docAll.localizedGroupRel.es.email).toBe(rel_2.id)
})
it('should insert/update/read localized group with hasMany relationship inside', async () => {
const rel_1 = await payload.create({
collection: 'email-fields',
data: { email: 'pro123@gmail.com' },
})
const rel_2 = await payload.create({
collection: 'email-fields',
data: { email: 'frank@gmail.com' },
})
const doc = await payload.create({
collection: 'group-fields',
depth: 0,
data: {
group: { text: 'requireddd' },
localizedGroupManyRel: {
email: [rel_1.id],
},
},
})
expect(doc.localizedGroupManyRel.email).toStrictEqual([rel_1.id])
const upd = await payload.update({
collection: 'group-fields',
depth: 0,
id: doc.id,
locale: 'es',
data: {
localizedGroupManyRel: {
email: [rel_2.id],
},
},
})
expect(upd.localizedGroupManyRel.email).toStrictEqual([rel_2.id])
const docAll = await payload.findByID({
collection: 'group-fields',
id: doc.id,
locale: 'all',
depth: 0,
})
expect(docAll.localizedGroupManyRel.en.email).toStrictEqual([rel_1.id])
expect(docAll.localizedGroupManyRel.es.email).toStrictEqual([rel_2.id])
})
it('should insert/update/read localized group with poly relationship inside', async () => {
const rel_1 = await payload.create({
collection: 'email-fields',
data: { email: 'pro123@gmail.com' },
})
const rel_2 = await payload.create({
collection: 'email-fields',
data: { email: 'frank@gmail.com' },
})
const doc = await payload.create({
collection: 'group-fields',
depth: 0,
data: {
group: { text: 'requireddd' },
localizedGroupPolyRel: {
email: {
relationTo: 'email-fields',
value: rel_1.id,
},
},
},
})
expect(doc.localizedGroupPolyRel.email).toStrictEqual({
relationTo: 'email-fields',
value: rel_1.id,
})
const upd = await payload.update({
collection: 'group-fields',
depth: 0,
id: doc.id,
locale: 'es',
data: {
localizedGroupPolyRel: {
email: {
value: rel_2.id,
relationTo: 'email-fields',
},
},
},
})
expect(upd.localizedGroupPolyRel.email).toStrictEqual({
value: rel_2.id,
relationTo: 'email-fields',
})
const docAll = await payload.findByID({
collection: 'group-fields',
id: doc.id,
locale: 'all',
depth: 0,
})
expect(docAll.localizedGroupPolyRel.en.email).toStrictEqual({
value: rel_1.id,
relationTo: 'email-fields',
})
expect(docAll.localizedGroupPolyRel.es.email).toStrictEqual({
value: rel_2.id,
relationTo: 'email-fields',
})
})
it('should insert/update/read localized group with poly hasMany relationship inside', async () => {
const rel_1 = await payload.create({
collection: 'email-fields',
data: { email: 'pro123@gmail.com' },
})
const rel_2 = await payload.create({
collection: 'email-fields',
data: { email: 'frank@gmail.com' },
})
const doc = await payload.create({
collection: 'group-fields',
depth: 0,
data: {
group: { text: 'requireddd' },
localizedGroupPolyHasManyRel: {
email: [
{
relationTo: 'email-fields',
value: rel_1.id,
},
],
},
},
})
expect(doc.localizedGroupPolyHasManyRel.email).toStrictEqual([
{
relationTo: 'email-fields',
value: rel_1.id,
},
])
const upd = await payload.update({
collection: 'group-fields',
depth: 0,
id: doc.id,
locale: 'es',
data: {
localizedGroupPolyHasManyRel: {
email: [
{
value: rel_2.id,
relationTo: 'email-fields',
},
],
},
},
})
expect(upd.localizedGroupPolyHasManyRel.email).toStrictEqual([
{
value: rel_2.id,
relationTo: 'email-fields',
},
])
const docAll = await payload.findByID({
collection: 'group-fields',
id: doc.id,
locale: 'all',
depth: 0,
})
expect(docAll.localizedGroupPolyHasManyRel.en.email).toStrictEqual([
{
value: rel_1.id,
relationTo: 'email-fields',
},
])
expect(docAll.localizedGroupPolyHasManyRel.es.email).toStrictEqual([
{
value: rel_2.id,
relationTo: 'email-fields',
},
])
})
})
describe('tabs', () => {
let document
beforeEach(async () => {
document = await payload.create({
collection: tabsFieldsSlug,
data: tabsDoc,
})
})
it('should hot module reload and still be able to create', async () => {
const testDoc1 = await payload.findByID({
id: document.id,
collection: tabsFieldsSlug,
})
await reload(payload.config, payload, true)
const testDoc2 = await payload.findByID({
id: document.id,
collection: tabsFieldsSlug,
})
expect(testDoc1.id).toStrictEqual(testDoc2.id)
})
it('should create with fields inside a named tab', () => {
expect(document.tab.text).toStrictEqual(namedTabText)
})
it('should create with defaultValue inside a named tab', () => {
expect(document.tab.defaultValue).toStrictEqual(namedTabDefaultValue)
})
it('should create with defaultValue inside a named tab with no other values', () => {
expect(document.namedTabWithDefaultValue.defaultValue).toStrictEqual(namedTabDefaultValue)
})
it('should create with localized text inside a named tab', async () => {
document = await payload.findByID({
id: document.id,
collection: tabsFieldsSlug,
locale: 'all',
})
expect(document.localizedTab.en.text).toStrictEqual(localizedTextValue)
})
it('should allow access control on a named tab', async () => {
document = await payload.findByID({
id: document.id,
collection: tabsFieldsSlug,
overrideAccess: false,
})
expect(document.accessControlTab).toBeUndefined()
})
it('should allow hooks on a named tab', async () => {
const newDocument = await payload.create({
collection: tabsFieldsSlug,
data: tabsDoc,
})
expect(newDocument.hooksTab.beforeValidate).toBe(true)
expect(newDocument.hooksTab.beforeChange).toBe(true)
expect(newDocument.hooksTab.afterChange).toBe(true)
expect(newDocument.hooksTab.afterRead).toBe(true)
})
it('should return empty object for groups when no data present', async () => {
const doc = await payload.create({
collection: groupFieldsSlug,
data: namedGroupDoc,
})
expect(doc.potentiallyEmptyGroup).toBeDefined()
})
it('should insert/read camelCase tab with nested arrays + localized', async () => {
const res = await payload.create({
collection: 'tabs-fields',
data: {
anotherText: 'req',
array: [{ text: 'req' }],
blocks: [{ blockType: 'content', text: 'req' }],
group: { number: 1 },
numberInRow: 1,
textInRow: 'req',
tab: { array: [{ text: 'req' }] },
camelCaseTab: {
array: [
{
text: 'text',
array: [
{
text: 'nested',
},
],
},
],
},
},
})
expect(res.camelCaseTab.array[0].text).toBe('text')
expect(res.camelCaseTab.array[0].array[0].text).toBe('nested')
})
it('should show proper validation error message on text field within array within tab', async () => {
await expect(async () =>
payload.update({
id: document.id,
collection: tabsFieldsSlug,
data: {
array: [
{
text: 'one',
},
{
text: 'two',
},
{
text: '',
},
],
},
}),
).rejects.toThrow('The following field is invalid: Tab with Array > Array 3 > Text')
})
})
describe('blocks', () => {
it('should retrieve doc with blocks', async () => {
const blockFields = await payload.find({
collection: 'block-fields',
})
expect(blockFields.docs[0].blocks[0].blockType).toEqual(blocksDoc.blocks[0].blockType)
expect(blockFields.docs[0].blocks[0].text).toEqual(blocksDoc.blocks[0].text)
expect(blockFields.docs[0].blocks[2].blockType).toEqual(blocksDoc.blocks[2].blockType)
expect(blockFields.docs[0].blocks[2].blockName).toEqual(blocksDoc.blocks[2].blockName)
expect(blockFields.docs[0].blocks[2].subBlocks[0].number).toEqual(
blocksDoc.blocks[2].subBlocks[0].number,
)
expect(blockFields.docs[0].blocks[2].subBlocks[1].text).toEqual(
blocksDoc.blocks[2].subBlocks[1].text,
)
})
it('should query based on richtext data within a block', async () => {
const blockFieldsSuccess = await payload.find({
collection: 'block-fields',
where: {
'blocks.richText.children.text': {
like: 'fun',
},
},
})
expect(blockFieldsSuccess.docs).toHaveLength(1)
const blockFieldsFail = await payload.find({
collection: 'block-fields',
where: {
'blocks.richText.children.text': {
like: 'funny',
},
},
})
expect(blockFieldsFail.docs).toHaveLength(0)
})
it('should query based on richtext data within a localized block, specifying locale', async () => {
const blockFieldsSuccess = await payload.find({
collection: 'block-fields',
where: {
'localizedBlocks.en.richText.children.text': {
like: 'fun',
},
},
})
expect(blockFieldsSuccess.docs).toHaveLength(1)
const blockFieldsFail = await payload.find({
collection: 'block-fields',
where: {
'localizedBlocks.en.richText.children.text': {
like: 'funny',
},
},
})
expect(blockFieldsFail.docs).toHaveLength(0)
})
it('should query based on richtext data within a localized block, without specifying locale', async () => {
const blockFieldsSuccess = await payload.find({
collection: 'block-fields',
where: {
'localizedBlocks.richText.children.text': {
like: 'fun',
},
},
})
expect(blockFieldsSuccess.docs).toHaveLength(1)
const blockFieldsFail = await payload.find({
collection: 'block-fields',
where: {
'localizedBlocks.richText.children.text': {
like: 'funny',
},
},
})
expect(blockFieldsFail.docs).toHaveLength(0)
})
it('should filter based on nested block fields', async () => {
await payload.create({
collection: 'block-fields',
data: {
blocks: [
{
blockType: 'content',
text: 'green',
},
],
},
})
await payload.create({
collection: 'block-fields',
data: {
blocks: [
{
blockType: 'content',
text: 'pink',
},
],
},
})
await payload.create({
collection: 'block-fields',
data: {
blocks: [
{
blockType: 'content',
text: 'green',
},
],
},
})
const blockFields = await payload.find({
collection: 'block-fields',
overrideAccess: false,
user,
where: {
and: [
{
'blocks.text': {
equals: 'green',
},
},
],
},
})
const { docs } = blockFields
expect(docs).toHaveLength(2)
})
it('should query blocks with nested relationship', async () => {
const textDoc = await payload.create({
collection: textFieldsSlug,
data: {
text: 'test',
},
})
const blockDoc = await payload.create({
collection: blockFieldsSlug,
data: {
relationshipBlocks: [
{
blockType: 'relationships',
relationship: textDoc.id,
},
],
},
})
const result = await payload.find({
collection: blockFieldsSlug,
where: {
'relationshipBlocks.relationship': { equals: textDoc.id },
},
})
expect(result.docs).toHaveLength(1)
expect(result.docs[0]).toMatchObject(blockDoc)
})
it('should query by blockType', async () => {
const text = 'blockType query test'
const hit = await payload.create({
collection: blockFieldsSlug,
data: {
blocks: [
{
blockType: 'content',
text,
},
],
},
})
const miss = await payload.create({
collection: blockFieldsSlug,
data: {
blocks: [
{
blockType: 'number',
number: 5,
},
],
duplicate: [
{
blockType: 'content',
text,
},
],
},
})
const { docs: equalsDocs } = await payload.find({
collection: blockFieldsSlug,
where: {
and: [
{
'blocks.blockType': { equals: 'content' },
},
{
'blocks.text': { equals: text },
},
],
},
})
const { docs: inDocs } = await payload.find({
collection: blockFieldsSlug,
where: {
'blocks.blockType': { in: ['content'] },
},
})
const equalsHitResult = equalsDocs.find(({ id }) => id === hit.id)
const inHitResult = inDocs.find(({ id }) => id === hit.id)
const equalsMissResult = equalsDocs.find(({ id }) => id === miss.id)
const inMissResult = inDocs.find(({ id }) => id === miss.id)
expect(equalsHitResult.id).toStrictEqual(hit.id)
expect(inHitResult.id).toStrictEqual(hit.id)
expect(equalsMissResult).toBeUndefined()
expect(inMissResult).toBeUndefined()
})
it('should allow localized array of blocks', async () => {
const result = await payload.create({
collection: blockFieldsSlug,
data: {
blocksWithLocalizedArray: [
{
blockType: 'localizedArray',
array: [
{
text: 'localized',
},
],
},
],
},
})
expect(result.blocksWithLocalizedArray[0].array[0].text).toEqual('localized')
})
it('ensure localized field within block reference is saved correctly', async () => {
const blockFields = await payload.find({
collection: 'block-fields',
locale: 'all',
})
const doc: BlockField = blockFields.docs[0] as BlockField
expect(doc?.localizedReferences?.[0]?.blockType).toEqual('localizedTextReference2')
expect(doc?.localizedReferences?.[0]?.text).toEqual({ en: 'localized text' })
})
it('ensure localized property is stripped from localized field within localized block reference', async () => {
const blockFields = await payload.find({
collection: 'block-fields',
locale: 'all',
})
const doc: any = blockFields.docs[0]
expect(doc?.localizedReferencesLocalizedBlock?.en?.[0]?.blockType).toEqual(
'localizedTextReference',
)
expect(doc?.localizedReferencesLocalizedBlock?.en?.[0]?.text).toEqual('localized text')
})
})
describe('collapsible', () => {
it('should show proper validation error message for fields nested in collapsible', async () => {
await expect(async () =>
payload.create({
collection: collapsibleFieldsSlug,
data: {
text: 'required',
group: {
subGroup: {
requiredTextWithinSubGroup: '',
},
},
},
}),
).rejects.toThrow(
'The following field is invalid: Collapsible Field > Group > Sub Group > Required Text Within Sub Group',
)
})
})
describe('json', () => {
it('should save json data', async () => {
const json = { foo: 'bar' }
const doc = await payload.create({
collection: 'json-fields',
data: {
json,
},
})
expect(doc.json).toStrictEqual({ foo: 'bar' })
})
it('should validate json', async () => {
await expect(async () =>
payload.create({
collection: 'json-fields',
data: {
json: '{ bad input: true }',
},
}),
).rejects.toThrow('The following field is invalid: Json')
})
it('should validate json schema', async () => {
await expect(async () =>
payload.create({
collection: 'json-fields',
data: {
json: { foo: 'bad' },
},
}),
).rejects.toThrow('The following field is invalid: Json')
})
it('should save empty json objects', async () => {
const jsonFieldsDoc = await payload.create({
collection: 'json-fields',
data: {
json: {
state: {},
},
},
})
expect(jsonFieldsDoc.json.state).toEqual({})
const updatedJsonFieldsDoc = await payload.update({
id: jsonFieldsDoc.id,
collection: 'json-fields',
data: {
json: {
state: {},
},
},
})
expect(updatedJsonFieldsDoc.json.state).toEqual({})
})
describe('querying', () => {
let fooBar
let bazBar
beforeEach(async () => {
fooBar = await payload.create({
collection: 'json-fields',
data: {
json: { foo: 'foobar', number: 5 },
},
})
bazBar = await payload.create({
collection: 'json-fields',
data: {
json: { baz: 'bar', number: 10 },
},
})
// Create content for array 'in' and 'not_in' queries
for (let i = 1; i < 6; i++) {
await payload.create({
collection: 'json-fields',
data: {
json: { value: i },
},
})
}
})
it('should query nested properties - like', async () => {
const { docs } = await payload.find({
collection: 'json-fields',
where: {
'json.foo': { like: 'bar' },
},
})
const docIDs = docs.map(({ id }) => id)
expect(docIDs).toContain(fooBar.id)
expect(docIDs).not.toContain(bazBar.id)
})
it('should query nested properties - not_like', async () => {
const { docs } = await payload.find({
collection: 'json-fields',
where: {
'json.baz': { not_like: 'bar' },
},
})
const docIDs = docs.map(({ id }) => id)
expect(docIDs).toContain(fooBar.id)
expect(docIDs).not.toContain(bazBar.id)
})
it('should query nested properties - equals', async () => {
const { docs } = await payload.find({
collection: 'json-fields',
where: {
'json.foo': { equals: 'foobar' },
},
})
const notEquals = await payload.find({
collection: 'json-fields',
where: {
'json.foo': { equals: 'bar' },
},
})
const docIDs = docs.map(({ id }) => id)
expect(docIDs).toContain(fooBar.id)
expect(docIDs).not.toContain(bazBar.id)
expect(notEquals.docs).toHaveLength(0)
})
it('should query nested numbers - equals', async () => {
const { docs } = await payload.find({
collection: 'json-fields',
where: {
'json.number': { equals: 5 },
},
})
const docIDs = docs.map(({ id }) => id)
expect(docIDs).toContain(fooBar.id)
expect(docIDs).not.toContain(bazBar.id)
})
it('should query nested properties - exists', async () => {
const { docs } = await payload.find({
collection: 'json-fields',
where: {
'json.foo': { exists: true },
},
})
const docIDs = docs.map(({ id }) => id)
expect(docIDs).toContain(fooBar.id)
expect(docIDs).not.toContain(bazBar.id)
})
it('should query - exists', async () => {
const nullJSON = await payload.create({
collection: 'json-fields',
data: {},
})
const hasJSON = await payload.create({
collection: 'json-fields',
data: {
json: [],
},
})
const docsExistsFalse = await payload.find({
collection: 'json-fields',
where: {
json: { exists: false },
},
})
const docsExistsTrue = await payload.find({
collection: 'json-fields',
where: {
json: { exists: true },
},
})
const existFalseIDs = docsExistsFalse.docs.map(({ id }) => id)
const existTrueIDs = docsExistsTrue.docs.map(({ id }) => id)
expect(existFalseIDs).toContain(nullJSON.id)
expect(existTrueIDs).not.toContain(nullJSON.id)
expect(existTrueIDs).toContain(hasJSON.id)
expect(existFalseIDs).not.toContain(hasJSON.id)
})
it('exists should not return null values', async () => {
const { id } = await payload.create({
collection: 'select-fields',
data: {
select: 'one',
},
})
const existsResult = await payload.find({
collection: 'select-fields',
where: {
id: { equals: id },
select: { exists: true },
},
})
expect(existsResult.docs).toHaveLength(1)
const existsFalseResult = await payload.find({
collection: 'select-fields',
where: {
id: { equals: id },
select: { exists: false },
},
})
expect(existsFalseResult.docs).toHaveLength(0)
await payload.update({
id,
collection: 'select-fields',
data: {
select: null,
},
})
const existsTrueResult = await payload.find({
collection: 'select-fields',
where: {
id: { equals: id },
select: { exists: true },
},
})
expect(existsTrueResult.docs).toHaveLength(0)
const result = await payload.find({
collection: 'select-fields',
where: {
id: { equals: id },
select: { exists: false },
},
})
expect(result.docs).toHaveLength(1)
})
it('should query nested numbers - in', async () => {
const { docs } = await payload.find({
collection: 'json-fields',
where: {
'json.value': { in: [1, 3] },
},
})
const docIDs = docs.map(({ json }) => json.value)
expect(docIDs).toContain(1)
expect(docIDs).toContain(3)
expect(docIDs).not.toContain(2)
})
it('should query nested numbers - not_in', async () => {
const { docs } = await payload.find({
collection: 'json-fields',
where: {
'json.value': { not_in: [1, 3] },
},
})
const docIDs = docs.map(({ json }) => json.value)
expect(docIDs).not.toContain(1)
expect(docIDs).not.toContain(3)
expect(docIDs).toContain(2)
})
it('should query deeply', async () => {
// eslint-disable-next-line jest/no-conditional-in-test
if (payload.db.name === 'sqlite') {
return
}
const json_1 = await payload.create({
collection: 'json-fields',
data: {
json: {
array: [
{
text: 'some-text',
object: {
text: 'deep-text',
array: [10],
},
},
],
},
},
})
const { docs } = await payload.find({
collection: 'json-fields',
where: {
and: [
{
'json.array.text': {
equals: 'some-text',
},
},
{
'json.array.object.text': {
equals: 'deep-text',
},
},
{
'json.array.object.array': {
in: [10, 20],
},
},
{
'json.array.object.array': {
exists: true,
},
},
{
'json.array.object.notexists': {
exists: false,
},
},
],
},
})
expect(docs).toHaveLength(1)
expect(docs[0].id).toBe(json_1.id)
})
})
})
describe('relationships', () => {
it('should not crash if querying with empty in operator', async () => {
const query = await payload.find({
collection: 'relationship-fields',
where: {
'relationship.value': {
in: [],
},
},
})
expect(query.docs).toBeDefined()
})
})
describe('clearable fields - exists', () => {
it('exists should not return null values', async () => {
const { id } = await payload.create({
collection: 'select-fields',
data: {
select: 'one',
},
})
const existsResult = await payload.find({
collection: 'select-fields',
where: {
id: { equals: id },
select: { exists: true },
},
})
expect(existsResult.docs).toHaveLength(1)
const existsFalseResult = await payload.find({
collection: 'select-fields',
where: {
id: { equals: id },
select: { exists: false },
},
})
expect(existsFalseResult.docs).toHaveLength(0)
await payload.update({
id,
collection: 'select-fields',
data: {
select: null,
},
})
const existsTrueResult = await payload.find({
collection: 'select-fields',
where: {
id: { equals: id },
select: { exists: true },
},
})
expect(existsTrueResult.docs).toHaveLength(0)
const result = await payload.find({
collection: 'select-fields',
where: {
id: { equals: id },
select: { exists: false },
},
})
expect(result.docs).toHaveLength(1)
})
})
})