Files
payloadcms/test/fields/int.spec.ts
Dan Ribbens b9dec2f714 Chore/next poc merge main (#5204)
* wip moves payload, user and data into partial req

* chore: adjust req type

* chore(next): installs sass and resolves type errors

* feat: working login route/view

* fix: me route

* chore(next): scaffolds access routes (#4562)

* chore(next): scaffolds admin layout and dashboard view (#4566)

* chore(next): builds initPage utility (#4589)

* feat(3.0): next route handlers (#4590)

* chore: removes old files

* chore(next): ssr list view (#4594)

* chore: removes old files

* chore: adjusts graphql file imports to align with new operation exports

* chore: allows for custom endpoints

* chore: cleanup

* chore(next): ssr edit view (#4614)

* chore(ui): ssr main nav (#4619)

* chore(next): ssr account view (#4620)

* chore(next): ssr auth views and document create (#4631)

* chore(next): ssr globals view (#4640)

* chore(next): scaffolds document layout (#4644)

* chore(next): ssr versions view (#4645)

* chore(next): ssr field conditions (#4675)

* chore(next): ssr field validations (#4700)

* chore(next): moves dashboard view into next dir

* chore(next): moves account view into next dir

* chore(next): moves global edit view into next dir

* chore(next): returns isolated configs and locale from initPage

* chore(next): ssr api view (#4721)

* feat: adds i18n functionality within Rest API, Local and Client contexts (#4749)

* chore: separate client translation groups with empty line

* chore: add missing translation used in db adapters

* chore: simplify next/routes export and import paths

* chore: renames PayloadT to Payload

* chore(next): custom views (#4748)

* chore: fix translation tsconfig

* chore: adjust other package ts-configs that rely on translations

* chore(next): installs @payloadcms/ui as direct dependency

* chore(next): progress to build

* chore(next): migrates types (#4792)

* fixes acccept-language detection

* chore(next): moves remaining components out from payload core (#4794)

* chore(deps): removes all unused dependencies from payload core (#4797)

* chore(next): achieves buildable state (#4803)

* adds Translation component and removes more react-i18next

* fixes up remaining translation strings

* fixes a few i18n TODO's

* chore: remaining translation strings without colons

* chore: adds missing ja translations

* chore(next): ssr group field (#4830)

* chore: removes placeholder t function

* chore: removes old file

* chore(bundler-webpack): removes webpack bundler

* chore(bundler-vite): removes vite bundler

* chore(next): ssr tabs field (#4863)

* chore(next): ssr row field

* chore(next): ssr textarea field

* chore(next): wires server action into document edit view (#4873)

* chore(next): conditional logic (#4880)

* chore(next): ssr radio, point, code, json, ui, and hidden fields (#4891)

* chore(next): ssr collapsible field (#4894)

* chore: remove findByID from req

* chore: adjusts file property on request type

* comment clarification

* chore: wires up busboy with Requst readstream

* chore: ports over express-fileupload into a NextJS compatible format

* chore: adjust upload file structure

* chore: adds try/catch around routes, corrects a few route responses

* chore: renames file/function

* chore: improve req type safety in local operations, misc req.files replacements

* chore: misc type and fn export changes

* chore: ensures root routes take pass unmodified request to root routes

* chore: improve types

* chore: consolidates locale api req initialization (#4922)

* chore(next): overhauls field rendering strategy (#4924)

* chore(next): ssr array field (#4937)

* chore(next): ssr blocks field (#4942)

* chore(next): ssr upload field and document drawer (#4957)

* chore(next): wires form submissions (#4982)

* chore: api handler adjustments

* feat: adds graphql playground handler

* adds credentials include setting to playground

* remove old playground init, stub graphql handler location

* fix: allow for null fallbackLocale

* fix: correctly prioritize locales passed as null

* chore: move all graphql code into next package

* graphql changes

* chore: semi working version of graphql http layer

* gql fix attempts

* rm console log

* chore: partial gql changes

* chore: adds gql and gql-http back into payload

* chore: removes collection from req

* chore: separates graphql package out for schema generation

* chore: dep cleanup

* chore: move graphql handlers

* chore: removes unused deps

* chore(next): ssr list view (#5032)

* chore: refactor response handler order for custom endpoints

* chore: add back in condition for collection GET path with 2 slugs

* chore: rm optional chain

* chore: import sort route file

* chore: allows custom endpoints to attempt before erroring

* feat: adds memoization to translation functions (#5036)

* chore: fix APIError import

* chore: return attemptCustomEndpointBeforeError responses

* chore(next): properly instantiates table columns

* fix(next): attaches params to req and properly assigns prefs key (#5042)

* chore: reorganize next route order

* chore(next): adds RouteError handler to next routes

* chore: builds payload successfully

* chore: misc file omissions

* fix(ui): maintains proper column order

* fix(ui): ensures first cell is a link

* fix(next): properly copies url object in createPayloadRequest (#5064)

* fix(ui): bumps react-toastify to v10.0.4 to fix hydration warnings

* feat: add route for static file GET requests (#5065)

* chore(next): allows resolved config promise to be thread through initPage (#5071)

* chore(ui): conditionally renders field label from props

* feat(next): next install script

* chore: pass config to route handlers

* feat: initial test suite framework (#4929)

* chore(next): renderable account, api, and create first user views (#5084)

* fix(next): properly parses search params in find, update, and delete handlers (#5088)

* chore(next): ssr versions view (#5085)

* chore: adds homepage for scss testing

* chore: moves dev folder to top, establishes new test pattern

* chore: working turbopack

* chore: sets up working dynamic payload-config imports

* remove unused code

* chore: rm console log

* misc

* feat: correctly subs out ability to boot REST API within same process

* chore: WIP dev suites

* chore: removes need for REST_API folder in test dir

* removes duplicate bootAdminPanel fn

* misc

* specify default export

* chore: sets up jest to work with next/jest

* chore: progress to mongodb and sharp builds

* chore: passing community tests

* chore: sorta workin

* chore: adjust payload-config import

* chore: adds rest client for Next handlers

* chore: removes test garb

* chore: restores payload-config tsconfig path temporarily

* chore: establishes pattern for memory db during tests

* chore: bumps mongoose to 7

* chore(next): 404s on nested create urls

* chore: functional _community e2e

* chore: increases e2e expect timeout

* fix(next): sanitizes locale toString from client config

* chore: type fixes

* chore: pulls mongodb from main

* chore: uses graphql to log user in

* feat: passing auth test suite

* chore(ui): threads params through context and conditionally renders document tabs (#5094)

* feat(ui): adds params context (#5095)

* chore: removes unecessary memory allocation for urlPropertiesObject object

* chore: passing graphql test suite

* chore: removes references to bson

* chore: re-enables mongodb memory server for auth test suite

* chore: replace bson with bson-objectid

* feat: passing collections-rest int suite

* chore: fixes bad imports

* chore: more passing int suites

* feat: passing globals int tests

* feat: passing hooks int test suite

* chore: remove last express file

* chore: start live-preview int test migration

* chore: passing localization int tests

* passing relationships int tests

* chore: partial passing upload int tests

* chore: fixes scss imports

* chore(ui): renders document info provider at root (#5106)

* chore: adds schema path to useFieldPath provider, more passing tests

* chore: begins work to optimize translation imports

* chore: add translations to ui ts-config references

* chore: add exports folder to package json exports

* chore: adds readme how-to-use instructions

* chore: attempts refactor of translation imports

* chore: adds authentication:account translation key to server keys

* chore: finishes translation optimization

* chore: ignores warnings from mongodb

* chore(ui): renders live document title (#5115)

* chore(ui): ssr document tabs (#5116)

* chore: handles redirecting from login

* chore: handle redirect with no searchParams

* chore: handle missing segments

* chore(next): migrates server action into standalone api endpoint (#5122)

* chore: adjust dashboard colection segments

* test: update e2e suites

* fix(ui): prevents unnecessary calls to form state

* chore: fix finding global config fields from schema path

* fix(next): executes root POST endpoints

* chore(ui): ignores values returned by form state polling

* chore: scaffolds ssr rte

* chore: renders client leaves

* chore: server-side rendered rich text elements

* chore: defines ClientFunction pattern

* chore(ui): migrates relationship field

* chore: adds translations, cleans up slate

* chore: functional slate link

* chore: slate upload ssr

* chore: relationship slate ssr

* chore: remaining slate ssr

* chore: fixes circular workspace dep

* chore: correct broken int test import paths

* chore: remove media files from root

* chore: server renders custom edit view

* fix(ui): resolves infinite loading in versions view

* fix(next): resolves global edit view lookup

* chore: payload builds

* chore: delete unused files

* chore: removes local property from payload

* chore: adds mongodb as dev dep in db-mongodb package

* chore: hide deprecation warnings for tempfile and jest-environment-jsdom

* chore: remove all translations from translations dist

* chore: clean ts-config files

* chore: simple type fixes

* chore(ui): server renders custom list view

* chore: fix next config payload-config alias

* chore: adds turbo alias paths

* chore: adjusts translation generation

* chore: improve auth function

* chore: eslint config for packages/ui

* chore(ui): exports FormState

* chore(next): migrates account view to latest patterns

* chore: disable barbie mode

* chore(ui): lints

* chore(next): lints

* chore: for alexical

* chore: custom handler type signature adjustment

* fix: non-boolean condition result causes infinite looping (#4579)

* chore(richtext-lexical): upgrade lexical from v0.12.5 to v0.12.6 (#4732)

* chore(richtext-lexical): upgrade all lexical packages from 0.12.5 to 0.12.6

* fix(richtext-lexical): fix TypeScript errors

* fix indenting

* feat(richtext-lexical): Blocks: generate type definitions for blocks fields (#4529)

* feat(richtext-lexical)!: Update lexical from 0.12.6 to 0.13.1, port over all useful changes from playground (#5066)

* feat(richtext-lexical): Update lexical from 0.12.6 to 0.13.1, port over all useful changes from playground

* chore: upgrade lexical version used in monorepo

* chore: remove the 3

* chore: upgrade nodemon versions (#5059)

* feat: add more options to addFieldStatePromise so that it can be used for field flattening (#4799)

* feat(plugin-seo)!: remove support for payload <2.7.0 (#4765)

* chore(plugin-seo): remove test script from package.json (#4762)

* chore: upgrade @types/nodemailer from v6.4.8 to v6.4.14 (#4733)

* chore: revert auth and initPage changes

* chore(next): moves edit and list views (#5170)

* fix: "The punycode module is deprecated" warning by updating nodemailer

* chore: adjust translations tsconfig paths in root

* chore: fix merge build

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Co-authored-by: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com>
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
Co-authored-by: James <james@trbl.design>
Co-authored-by: Alessio Gravili <alessio@gravili.de>
Co-authored-by: Alessio Gravili <70709113+AlessioGr@users.noreply.github.com>
2024-02-28 13:44:17 -05:00

1498 lines
40 KiB
TypeScript

import type { IndexDirection, IndexOptions } from 'mongoose'
import type { MongooseAdapter } from '../../packages/db-mongodb/src/index'
import type { Payload } from '../../packages/payload/src'
import type { PaginatedDocs } from '../../packages/payload/src/database/types'
import type { GroupField, RichTextField } from './payload-types'
import type { GroupField } from './payload-types'
import { getPayload } from '../../packages/payload/src'
import { devUser } from '../credentials'
import { NextRESTClient } from '../helpers/NextRESTClient'
import { isMongoose } from '../helpers/isMongoose'
import { startMemoryDB } from '../startMemoryDB'
import { arrayDefaultValue } from './collections/Array'
import { blocksDoc } from './collections/Blocks/shared'
import { dateDoc } from './collections/Date/shared'
import { groupDefaultChild, groupDefaultValue } from './collections/Group'
import { groupDoc } from './collections/Group/shared'
import { defaultNumber } from './collections/Number'
import { numberDoc } from './collections/Number/shared'
import { pointDoc } from './collections/Point/shared'
import {
localizedTextValue,
namedTabDefaultValue,
namedTabText,
} from './collections/Tabs/constants'
import { tabsDoc } from './collections/Tabs/shared'
import { defaultText } from './collections/Text/shared'
import configPromise from './config'
import { clearAndSeedEverything } from './seed'
import {
arrayFieldsSlug,
blockFieldsSlug,
groupFieldsSlug,
relationshipFieldsSlug,
tabsFieldsSlug,
textFieldsSlug,
} from './slugs'
let restClient: NextRESTClient
let user: any
let payload: Payload
describe('Fields', () => {
beforeAll(async () => {
const config = await startMemoryDB(configPromise)
payload = await getPayload({ config })
restClient = new NextRESTClient(payload.config)
await restClient.login({
slug: 'users',
credentials: devUser,
})
user = await payload.login({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
})
beforeEach(async () => {
await clearAndSeedEverything(payload)
await restClient.login({
slug: 'users',
credentials: devUser,
})
})
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).toEqual(text)
expect(doc.defaultFunction).toEqual(defaultText)
expect(doc.defaultAsync).toEqual(defaultText)
})
it('should populate default values in beforeValidate hook', async () => {
const { dependentOnFieldWithDefaultValue, fieldWithDefaultValue } = await payload.create({
collection: 'text-fields',
data: { text },
})
await expect(fieldWithDefaultValue).toEqual(dependentOnFieldWithDefaultValue)
})
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',
})
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(localizedDoc.localizedHasMany.en).toEqual(localizedHasMany)
})
})
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)
})
})
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)
})
})
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'])
})
})
describe('number', () => {
let doc
beforeEach(async () => {
doc = await payload.create({
collection: 'number-fields',
data: numberDoc,
})
})
it('creates with default values', async () => {
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: positiveNumber')
})
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: negativeNumber')
})
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: decimalMin')
})
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: decimalMax')
})
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',
})
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(localizedDoc.localizedHasMany.en).toEqual(localizedHasMany)
})
})
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]
// eslint-disable-next-line prefer-destructuring
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]
// eslint-disable-next-line prefer-destructuring
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 () => {
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 () => {
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 () => {
// 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)
})
})
}
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 not throw validation error saving multiple null values for unique fields', async () => {
const data = {
text: 'a',
uniqueRequiredText: 'a',
// uniqueText omitted on purpose
}
await payload.create({
collection: 'indexed-fields',
data,
})
data.uniqueRequiredText = 'b'
const result = await payload.create({
collection: 'indexed-fields',
data,
})
expect(result.id).toBeDefined()
})
})
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: groupDoc,
})) as Partial<GroupField>
expect(docWithIDs.group.subGroup.arrayWithinGroup[0].id).toBeDefined()
})
it('should create with defaultValue', async () => {
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 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)
})
})
describe('group', () => {
let document
beforeEach(async () => {
document = await payload.create({
collection: groupFieldsSlug,
data: {},
})
})
it('should create with defaultValue', async () => {
expect(document.group.defaultParent).toStrictEqual(groupDefaultValue)
expect(document.group.defaultChild).toStrictEqual(groupDefaultChild)
})
it('should not have duplicate keys', async () => {
expect(document.arrayOfGroups[0]).toMatchObject({
id: expect.any(String),
groupItem: {
text: 'Hello world',
},
})
})
})
describe('tabs', () => {
let document
beforeEach(async () => {
document = await payload.create({
collection: tabsFieldsSlug,
data: tabsDoc,
})
})
it('should create with fields inside a named tab', async () => {
expect(document.tab.text).toStrictEqual(namedTabText)
})
it('should create with defaultValue inside a named tab', async () => {
expect(document.tab.defaultValue).toStrictEqual(namedTabDefaultValue)
})
it('should create with defaultValue inside a named tab with no other values', async () => {
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: groupDoc,
})
expect(doc.potentiallyEmptyGroup).toBeDefined()
})
})
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 create when existing block ids are used', async () => {
const blockFields = await payload.find({
collection: 'block-fields',
limit: 1,
})
const [doc] = blockFields.docs
const result = await payload.create({
collection: 'block-fields',
data: {
...doc,
},
})
expect(result.id).toBeDefined()
})
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()
})
})
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 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 },
},
})
})
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 - 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)
})
})
})
describe('richText', () => {
it('should allow querying on rich text content', async () => {
const emptyRichTextQuery = await payload.find({
collection: 'rich-text-fields',
where: {
'richText.children.text': {
like: 'doesnt exist',
},
},
})
expect(emptyRichTextQuery.docs).toHaveLength(0)
const workingRichTextQuery = await payload.find({
collection: 'rich-text-fields',
where: {
'richText.children.text': {
like: 'hello',
},
},
})
expect(workingRichTextQuery.docs).toHaveLength(1)
})
it('should show center alignment', async () => {
const query = await payload.find({
collection: 'rich-text-fields',
where: {
'richText.children.text': {
like: 'hello',
},
},
})
expect(query.docs[0].richText[0].textAlign).toEqual('center')
})
it('should populate link relationship', async () => {
const query = await payload.find({
collection: 'rich-text-fields',
where: {
'richText.children.linkType': {
equals: 'internal',
},
},
})
const nodes = query.docs[0].richText
expect(nodes).toBeDefined()
const child = nodes.flatMap((n) => n.children).find((c) => c.doc)
expect(child).toMatchObject({
type: 'link',
linkType: 'internal',
})
expect(child.doc.relationTo).toEqual('array-fields')
if (payload.db.defaultIDType === 'number') {
expect(typeof child.doc.value.id).toBe('number')
} else {
expect(typeof child.doc.value.id).toBe('string')
}
expect(child.doc.value.items).toHaveLength(6)
})
it('should respect rich text depth parameter', async () => {
const query = `query {
RichTextFields {
docs {
richText(depth: 2)
}
}
}`
const { data } = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
})
.then((res) => res.json())
const { docs }: PaginatedDocs<RichTextField> = data.RichTextFields
const uploadElement = docs[0].richText.find((el) => el.type === 'upload') as any
expect(uploadElement.value.media.filename).toStrictEqual('payload.png')
})
})
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)
})
})
})