feat!: join field (#7518)
## Description
- Adds a new "join" field type to Payload and is supported by all database adapters
- The UI uses a table view for the new field
- `db-mongodb` changes relationships to be stored as ObjectIDs instead of strings (for now querying works using both types internally to the DB so no data migration should be necessary unless you're querying directly, see breaking changes for details
- Adds a reusable traverseFields utility to Payload to make it easier to work with nested fields, used internally and for plugin maintainers
```ts
export const Categories: CollectionConfig = {
slug: 'categories',
fields: [
{
name: 'relatedPosts',
type: 'join',
collection: 'posts',
on: 'category',
}
]
}
```
BREAKING CHANGES:
All mongodb relationship and upload values will be stored as MongoDB ObjectIDs instead of strings going forward. If you have existing data and you are querying data directly, outside of Payload's APIs, you get different results. For example, a `contains` query will no longer works given a partial ID of a relationship since the ObjectID requires the whole identifier to work.
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Co-authored-by: James <james@trbl.design>
This commit is contained in:
18
test/config/collections/Joins/index.ts
Normal file
18
test/config/collections/Joins/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const postsSlug = 'joins'
|
||||
export const PostsCollection: CollectionConfig = {
|
||||
slug: postsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'text',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
}
|
||||
@@ -198,7 +198,9 @@ export async function openNav(page: Page): Promise<void> {
|
||||
// check to see if the nav is already open and if not, open it
|
||||
// use the `--nav-open` modifier class to check if the nav is open
|
||||
// this will prevent clicking nav links that are bleeding off the screen
|
||||
if (await page.locator('.template-default.template-default--nav-open').isVisible()) {return}
|
||||
if (await page.locator('.template-default.template-default--nav-open').isVisible()) {
|
||||
return
|
||||
}
|
||||
// playwright: get first element with .nav-toggler which is VISIBLE (not hidden), could be 2 elements with .nav-toggler on mobile and desktop but only one is visible
|
||||
await page.locator('.nav-toggler >> visible=true').click()
|
||||
await expect(page.locator('.template-default.template-default--nav-open')).toBeVisible()
|
||||
@@ -221,7 +223,9 @@ export async function openCreateDocDrawer(page: Page, fieldSelector: string): Pr
|
||||
}
|
||||
|
||||
export async function closeNav(page: Page): Promise<void> {
|
||||
if (!(await page.locator('.template-default.template-default--nav-open').isVisible())) {return}
|
||||
if (!(await page.locator('.template-default.template-default--nav-open').isVisible())) {
|
||||
return
|
||||
}
|
||||
await page.locator('.nav-toggler >> visible=true').click()
|
||||
await expect(page.locator('.template-default.template-default--nav-open')).toBeHidden()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SanitizedConfig, Where } from 'payload'
|
||||
import type { JoinQuery, SanitizedConfig, Where } from 'payload'
|
||||
import type { ParsedQs } from 'qs-esm'
|
||||
|
||||
import {
|
||||
@@ -18,6 +18,7 @@ type RequestOptions = {
|
||||
query?: {
|
||||
depth?: number
|
||||
fallbackLocale?: string
|
||||
joins?: JoinQuery
|
||||
limit?: number
|
||||
locale?: string
|
||||
page?: number
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
export async function navigateToListCellLink(page: Page, selector = '.cell-id') {
|
||||
const cellLink = page.locator(`${selector} a`).first()
|
||||
export async function navigateToListCellLink(page: Page) {
|
||||
const cellLink = page.locator(`tbody tr:first-child td a`).first()
|
||||
const linkURL = await cellLink.getAttribute('href')
|
||||
await cellLink.click()
|
||||
await page.waitForURL(`**${linkURL}`)
|
||||
|
||||
@@ -19,8 +19,12 @@ export const reorderColumns = async (
|
||||
togglerSelector?: string
|
||||
},
|
||||
) => {
|
||||
await page.locator(togglerSelector).click()
|
||||
const columnContainer = page.locator(columnContainerSelector)
|
||||
const columnContainer = page.locator(columnContainerSelector).first()
|
||||
const isAlreadyOpen = await columnContainer.isVisible()
|
||||
|
||||
if (!isAlreadyOpen) {
|
||||
await page.locator(togglerSelector).first().click()
|
||||
}
|
||||
|
||||
await expect(page.locator(`${columnContainerSelector}.rah-static--height-auto`)).toBeVisible()
|
||||
|
||||
@@ -49,7 +53,7 @@ export const reorderColumns = async (
|
||||
columnContainer.locator('.column-selector .column-selector__column').first(),
|
||||
).toHaveText(fromColumn)
|
||||
|
||||
await expect(page.locator('table thead tr th').nth(1)).toHaveText(fromColumn)
|
||||
await expect(page.locator('table thead tr th').nth(1).first()).toHaveText(fromColumn)
|
||||
// TODO: This wait makes sure the preferences are actually saved. Just waiting for the UI to update is not enough. We should replace this wait
|
||||
await wait(1000)
|
||||
}
|
||||
|
||||
66
test/joins/collections/Categories.ts
Normal file
66
test/joins/collections/Categories.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { categoriesSlug, postsSlug } from '../shared.js'
|
||||
|
||||
export const Categories: CollectionConfig = {
|
||||
slug: categoriesSlug,
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
// Alternative tabs usage
|
||||
// {
|
||||
// type: 'tabs',
|
||||
// tabs: [
|
||||
// {
|
||||
// label: 'Unnamed tab',
|
||||
// fields: [
|
||||
// {
|
||||
// name: 'relatedPosts',
|
||||
// label: 'Related Posts',
|
||||
// type: 'join',
|
||||
// collection: postsSlug,
|
||||
// on: 'category',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// name: 'group',
|
||||
// fields: [
|
||||
// {
|
||||
// name: 'relatedPosts',
|
||||
// label: 'Related Posts (Group)',
|
||||
// type: 'join',
|
||||
// collection: postsSlug,
|
||||
// on: 'group.category',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
name: 'relatedPosts',
|
||||
label: 'Related Posts',
|
||||
type: 'join',
|
||||
collection: postsSlug,
|
||||
on: 'category',
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'relatedPosts',
|
||||
label: 'Related Posts (Group)',
|
||||
type: 'join',
|
||||
collection: postsSlug,
|
||||
on: 'group.category',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
33
test/joins/collections/Posts.ts
Normal file
33
test/joins/collections/Posts.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { categoriesSlug, postsSlug } from '../shared.js'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: postsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'category', 'updatedAt', 'createdAt'],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
type: 'relationship',
|
||||
relationTo: categoriesSlug,
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
type: 'relationship',
|
||||
relationTo: categoriesSlug,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
68
test/joins/config.ts
Normal file
68
test/joins/config.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { Categories } from './collections/Categories.js'
|
||||
import { Posts } from './collections/Posts.js'
|
||||
import { seed } from './seed.js'
|
||||
import { localizedCategoriesSlug, localizedPostsSlug } from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [
|
||||
Posts,
|
||||
Categories,
|
||||
{
|
||||
slug: localizedPostsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
type: 'relationship',
|
||||
localized: true,
|
||||
relationTo: localizedCategoriesSlug,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: localizedCategoriesSlug,
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'relatedPosts',
|
||||
type: 'join',
|
||||
collection: localizedPostsSlug,
|
||||
on: 'category',
|
||||
localized: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
localization: {
|
||||
locales: ['en', 'es'],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
|
||||
await seed(payload)
|
||||
}
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
186
test/joins/e2e.spec.ts
Normal file
186
test/joins/e2e.spec.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { reorderColumns } from 'helpers/e2e/reorderColumns.js'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { ensureCompilationIsDone, exactText, initPageConsoleErrorCatch } from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { navigateToDoc } from '../helpers/e2e/navigateToDoc.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import { categoriesSlug, postsSlug } from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
test.describe('Admin Panel', () => {
|
||||
let page: Page
|
||||
let categoriesURL: AdminUrlUtil
|
||||
let postsURL: AdminUrlUtil
|
||||
|
||||
test.beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||
|
||||
const { payload, serverURL } = await initPayloadE2ENoConfig({ dirname })
|
||||
postsURL = new AdminUrlUtil(serverURL, postsSlug)
|
||||
categoriesURL = new AdminUrlUtil(serverURL, categoriesSlug)
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
await ensureCompilationIsDone({ page, serverURL })
|
||||
})
|
||||
|
||||
test('should populate joined relationships in table cells of list view', async () => {
|
||||
await page.goto(categoriesURL.list)
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
await page
|
||||
.locator('tbody tr:first-child td.cell-relatedPosts', {
|
||||
hasText: exactText('Test Post 3, Test Post 2, Test Post 1'),
|
||||
})
|
||||
.isVisible(),
|
||||
)
|
||||
.toBeTruthy()
|
||||
})
|
||||
|
||||
test('should render initial rows within relationship table', async () => {
|
||||
await navigateToDoc(page, categoriesURL)
|
||||
const joinField = page.locator('.field-type.join').first()
|
||||
await expect(joinField).toBeVisible()
|
||||
const columns = await joinField.locator('.relationship-table tbody tr').count()
|
||||
expect(columns).toBe(3)
|
||||
})
|
||||
|
||||
test('should render collection type in first column of relationship table', async () => {
|
||||
await navigateToDoc(page, categoriesURL)
|
||||
const joinField = page.locator('.field-type.join').first()
|
||||
await expect(joinField).toBeVisible()
|
||||
const collectionTypeColumn = joinField.locator('thead tr th#heading-collection:first-child')
|
||||
const text = collectionTypeColumn
|
||||
await expect(text).toHaveText('Type')
|
||||
const cells = joinField.locator('.relationship-table tbody tr td:first-child .pill__label')
|
||||
|
||||
const count = await cells.count()
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const element = cells.nth(i)
|
||||
// Perform actions on each element
|
||||
await expect(element).toBeVisible()
|
||||
await expect(element).toHaveText('Post')
|
||||
}
|
||||
})
|
||||
|
||||
test('should render drawer toggler without document link in second column of relationship table', async () => {
|
||||
await navigateToDoc(page, categoriesURL)
|
||||
const joinField = page.locator('.field-type.join').first()
|
||||
await expect(joinField).toBeVisible()
|
||||
const actionColumn = joinField.locator('tbody tr td:nth-child(2)').first()
|
||||
const toggler = actionColumn.locator('button.doc-drawer__toggler')
|
||||
await expect(toggler).toBeVisible()
|
||||
const link = actionColumn.locator('a')
|
||||
await expect(link).toBeHidden()
|
||||
|
||||
await reorderColumns(page, {
|
||||
togglerSelector: '.relationship-table__toggle-columns',
|
||||
columnContainerSelector: '.relationship-table__columns',
|
||||
fromColumn: 'Category',
|
||||
toColumn: 'Title',
|
||||
})
|
||||
|
||||
const newActionColumn = joinField.locator('tbody tr td:nth-child(2)').first()
|
||||
const newToggler = newActionColumn.locator('button.doc-drawer__toggler')
|
||||
await expect(newToggler).toBeVisible()
|
||||
const newLink = newActionColumn.locator('a')
|
||||
await expect(newLink).toBeHidden()
|
||||
|
||||
// put columns back in original order for the next test
|
||||
await reorderColumns(page, {
|
||||
togglerSelector: '.relationship-table__toggle-columns',
|
||||
columnContainerSelector: '.relationship-table__columns',
|
||||
fromColumn: 'Title',
|
||||
toColumn: 'Category',
|
||||
})
|
||||
})
|
||||
|
||||
test('should sort relationship table by clicking on column headers', async () => {
|
||||
await navigateToDoc(page, categoriesURL)
|
||||
const joinField = page.locator('.field-type.join').first()
|
||||
await expect(joinField).toBeVisible()
|
||||
const titleColumn = joinField.locator('thead tr th#heading-title')
|
||||
const titleAscButton = titleColumn.locator('button.sort-column__asc')
|
||||
await expect(titleAscButton).toBeVisible()
|
||||
await titleAscButton.click()
|
||||
await expect(joinField.locator('tbody tr:first-child td:nth-child(2)')).toHaveText(
|
||||
'Test Post 1',
|
||||
)
|
||||
|
||||
const titleDescButton = titleColumn.locator('button.sort-column__desc')
|
||||
await expect(titleDescButton).toBeVisible()
|
||||
await titleDescButton.click()
|
||||
await expect(joinField.locator('tbody tr:first-child td:nth-child(2)')).toHaveText(
|
||||
'Test Post 3',
|
||||
)
|
||||
})
|
||||
|
||||
test('should update relationship table when new document is created', async () => {
|
||||
await navigateToDoc(page, categoriesURL)
|
||||
const joinField = page.locator('.field-type.join').first()
|
||||
await expect(joinField).toBeVisible()
|
||||
|
||||
const addButton = joinField.locator('.relationship-table__actions button.doc-drawer__toggler', {
|
||||
hasText: exactText('Add new'),
|
||||
})
|
||||
|
||||
await expect(addButton).toBeVisible()
|
||||
|
||||
await addButton.click()
|
||||
const drawer = page.locator('[id^=doc-drawer_posts_1_]')
|
||||
await expect(drawer).toBeVisible()
|
||||
const categoryField = drawer.locator('#field-category')
|
||||
await expect(categoryField).toBeVisible()
|
||||
const categoryValue = categoryField.locator('.relationship--single-value__text')
|
||||
await expect(categoryValue).toHaveText('example')
|
||||
const titleField = drawer.locator('#field-title')
|
||||
await expect(titleField).toBeVisible()
|
||||
await titleField.fill('Test Post 4')
|
||||
await drawer.locator('button[id="action-save"]').click()
|
||||
await expect(drawer).toBeHidden()
|
||||
await expect(
|
||||
joinField.locator('tbody tr td:nth-child(2)', {
|
||||
hasText: exactText('Test Post 4'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should update relationship table when document is updated', async () => {
|
||||
await navigateToDoc(page, categoriesURL)
|
||||
const joinField = page.locator('.field-type.join').first()
|
||||
await expect(joinField).toBeVisible()
|
||||
const editButton = joinField.locator(
|
||||
'tbody tr:first-child td:nth-child(2) button.doc-drawer__toggler',
|
||||
)
|
||||
await expect(editButton).toBeVisible()
|
||||
await editButton.click()
|
||||
const drawer = page.locator('[id^=doc-drawer_posts_1_]')
|
||||
await expect(drawer).toBeVisible()
|
||||
const titleField = drawer.locator('#field-title')
|
||||
await expect(titleField).toBeVisible()
|
||||
await titleField.fill('Test Post 1 Updated')
|
||||
await drawer.locator('button[id="action-save"]').click()
|
||||
await expect(drawer).toBeHidden()
|
||||
await expect(joinField.locator('tbody tr:first-child td:nth-child(2)')).toHaveText(
|
||||
'Test Post 1 Updated',
|
||||
)
|
||||
})
|
||||
|
||||
test('should render empty relationship table when creating new document', async () => {
|
||||
await page.goto(categoriesURL.create)
|
||||
const joinField = page.locator('.field-type.join').first()
|
||||
await expect(joinField).toBeVisible()
|
||||
await expect(joinField.locator('.relationship-table tbody tr')).toBeHidden()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { rootParserOptions } from '../../eslint.config.js'
|
||||
import testEslintConfig from '../eslint.config.js'
|
||||
import { testEslintConfig } from '../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.FlatConfig} */
|
||||
let FlatConfig
|
||||
401
test/joins/int.spec.ts
Normal file
401
test/joins/int.spec.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
import type { Category, Post } from './payload-types.js'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { idToString } from '../helpers/idToString.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
let payload: Payload
|
||||
let token: string
|
||||
let restClient: NextRESTClient
|
||||
|
||||
const { email, password } = devUser
|
||||
|
||||
describe('Joins Field', () => {
|
||||
let category: Category
|
||||
let categoryID
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
// Boilerplate test setup/teardown
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
beforeAll(async () => {
|
||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
||||
|
||||
const data = await restClient
|
||||
.POST('/users/login', {
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
token = data.token
|
||||
|
||||
category = await payload.create({
|
||||
collection: 'categories',
|
||||
data: {
|
||||
name: 'paginate example',
|
||||
group: {},
|
||||
},
|
||||
})
|
||||
|
||||
categoryID = idToString(category.id, payload)
|
||||
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await createPost({
|
||||
title: `test ${i}`,
|
||||
category: category.id,
|
||||
group: {
|
||||
category: category.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (typeof payload.db.destroy === 'function') {
|
||||
await payload.db.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
it('should populate joins using findByID', async () => {
|
||||
const categoryWithPosts = await payload.findByID({
|
||||
id: category.id,
|
||||
joins: {
|
||||
'group.relatedPosts': {
|
||||
sort: '-title',
|
||||
},
|
||||
},
|
||||
collection: 'categories',
|
||||
})
|
||||
|
||||
expect(categoryWithPosts.group.relatedPosts.docs).toHaveLength(10)
|
||||
expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('id')
|
||||
expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('title')
|
||||
expect(categoryWithPosts.group.relatedPosts.docs[0].title).toStrictEqual('test 9')
|
||||
})
|
||||
|
||||
it('should populate relationships in joins', async () => {
|
||||
const { docs } = await payload.find({
|
||||
limit: 1,
|
||||
collection: 'posts',
|
||||
})
|
||||
|
||||
expect(docs[0].category.id).toBeDefined()
|
||||
expect(docs[0].category.name).toBeDefined()
|
||||
expect(docs[0].category.relatedPosts.docs).toHaveLength(10)
|
||||
})
|
||||
|
||||
it('should filter joins using where query', async () => {
|
||||
const categoryWithPosts = await payload.findByID({
|
||||
id: category.id,
|
||||
joins: {
|
||||
relatedPosts: {
|
||||
sort: '-title',
|
||||
where: {
|
||||
title: {
|
||||
equals: 'test 9',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
collection: 'categories',
|
||||
})
|
||||
|
||||
expect(categoryWithPosts.relatedPosts.docs).toHaveLength(1)
|
||||
expect(categoryWithPosts.relatedPosts.hasNextPage).toStrictEqual(false)
|
||||
})
|
||||
|
||||
it('should populate joins using find', async () => {
|
||||
const result = await payload.find({
|
||||
collection: 'categories',
|
||||
})
|
||||
|
||||
const [categoryWithPosts] = result.docs
|
||||
|
||||
expect(categoryWithPosts.group.relatedPosts.docs).toHaveLength(10)
|
||||
expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('title')
|
||||
expect(categoryWithPosts.group.relatedPosts.docs[0].title).toBe('test 14')
|
||||
})
|
||||
|
||||
describe('Joins with localization', () => {
|
||||
let localizedCategory: Category
|
||||
|
||||
beforeAll(async () => {
|
||||
localizedCategory = await payload.create({
|
||||
collection: 'localized-categories',
|
||||
locale: 'en',
|
||||
data: {
|
||||
name: 'localized category',
|
||||
},
|
||||
})
|
||||
const post1 = await payload.create({
|
||||
collection: 'localized-posts',
|
||||
locale: 'en',
|
||||
data: {
|
||||
title: 'english post 1',
|
||||
category: localizedCategory.id,
|
||||
},
|
||||
})
|
||||
await payload.update({
|
||||
collection: 'localized-posts',
|
||||
id: post1.id,
|
||||
locale: 'es',
|
||||
data: {
|
||||
title: 'spanish post',
|
||||
category: localizedCategory.id,
|
||||
},
|
||||
})
|
||||
await payload.create({
|
||||
collection: 'localized-posts',
|
||||
locale: 'en',
|
||||
data: {
|
||||
title: 'english post 2',
|
||||
category: localizedCategory.id,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should populate joins using findByID with localization on the relationship', async () => {
|
||||
const enCategory = await payload.findByID({
|
||||
id: localizedCategory.id,
|
||||
collection: 'localized-categories',
|
||||
locale: 'en',
|
||||
})
|
||||
const esCategory = await payload.findByID({
|
||||
id: localizedCategory.id,
|
||||
collection: 'localized-categories',
|
||||
locale: 'es',
|
||||
})
|
||||
expect(enCategory.relatedPosts.docs).toHaveLength(2)
|
||||
expect(esCategory.relatedPosts.docs).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('REST', () => {
|
||||
it('should have simple paginate for joins', async () => {
|
||||
const query = {
|
||||
depth: 1,
|
||||
where: {
|
||||
name: { equals: 'paginate example' },
|
||||
},
|
||||
joins: {
|
||||
relatedPosts: {
|
||||
sort: 'createdAt',
|
||||
limit: 4,
|
||||
},
|
||||
},
|
||||
}
|
||||
const pageWithLimit = await restClient.GET(`/categories`, { query }).then((res) => res.json())
|
||||
|
||||
query.joins.relatedPosts.limit = 0
|
||||
const unlimited = await restClient.GET(`/categories`, { query }).then((res) => res.json())
|
||||
|
||||
expect(pageWithLimit.docs[0].relatedPosts.docs).toHaveLength(4)
|
||||
expect(pageWithLimit.docs[0].relatedPosts.docs[0].title).toStrictEqual('test 0')
|
||||
expect(pageWithLimit.docs[0].relatedPosts.hasNextPage).toStrictEqual(true)
|
||||
|
||||
expect(unlimited.docs[0].relatedPosts.docs).toHaveLength(15)
|
||||
expect(unlimited.docs[0].relatedPosts.docs[0].title).toStrictEqual('test 0')
|
||||
expect(unlimited.docs[0].relatedPosts.hasNextPage).toStrictEqual(false)
|
||||
})
|
||||
|
||||
it('should sort joins', async () => {
|
||||
const response = await restClient
|
||||
.GET(`/categories/${category.id}?joins[relatedPosts][sort]=-title`)
|
||||
.then((res) => res.json())
|
||||
expect(response.relatedPosts.docs[0].title).toStrictEqual('test 9')
|
||||
})
|
||||
|
||||
it('should query in on collections with joins', async () => {
|
||||
const response = await restClient
|
||||
.GET(`/categories?where[id][in]=${category.id}`)
|
||||
.then((res) => res.json())
|
||||
expect(response.docs[0].name).toStrictEqual(category.name)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GraphQL', () => {
|
||||
it('should have simple paginate for joins', async () => {
|
||||
const queryWithLimit = `query {
|
||||
Categories(where: {
|
||||
name: { equals: "paginate example" }
|
||||
}) {
|
||||
docs {
|
||||
relatedPosts(
|
||||
sort: "createdAt",
|
||||
limit: 4
|
||||
) {
|
||||
docs {
|
||||
title
|
||||
}
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
const pageWithLimit = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query: queryWithLimit }) })
|
||||
.then((res) => res.json())
|
||||
|
||||
const queryUnlimited = `query {
|
||||
Categories(
|
||||
where: {
|
||||
name: { equals: "paginate example" }
|
||||
}
|
||||
) {
|
||||
docs {
|
||||
relatedPosts(
|
||||
sort: "createdAt",
|
||||
limit: 0
|
||||
) {
|
||||
docs {
|
||||
title
|
||||
createdAt
|
||||
}
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
const unlimited = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query: queryUnlimited }) })
|
||||
.then((res) => res.json())
|
||||
|
||||
expect(pageWithLimit.data.Categories.docs[0].relatedPosts.docs).toHaveLength(4)
|
||||
expect(pageWithLimit.data.Categories.docs[0].relatedPosts.docs[0].title).toStrictEqual(
|
||||
'test 0',
|
||||
)
|
||||
expect(pageWithLimit.data.Categories.docs[0].relatedPosts.hasNextPage).toStrictEqual(true)
|
||||
|
||||
expect(unlimited.data.Categories.docs[0].relatedPosts.docs).toHaveLength(15)
|
||||
expect(unlimited.data.Categories.docs[0].relatedPosts.docs[0].title).toStrictEqual('test 0')
|
||||
expect(unlimited.data.Categories.docs[0].relatedPosts.hasNextPage).toStrictEqual(false)
|
||||
})
|
||||
|
||||
it('should have simple paginate for joins inside groups', async () => {
|
||||
const queryWithLimit = `query {
|
||||
Categories(where: {
|
||||
name: { equals: "paginate example" }
|
||||
}) {
|
||||
docs {
|
||||
group {
|
||||
relatedPosts(
|
||||
sort: "createdAt",
|
||||
limit: 4
|
||||
) {
|
||||
docs {
|
||||
title
|
||||
}
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
const pageWithLimit = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query: queryWithLimit }) })
|
||||
.then((res) => res.json())
|
||||
|
||||
const queryUnlimited = `query {
|
||||
Categories(
|
||||
where: {
|
||||
name: { equals: "paginate example" }
|
||||
}
|
||||
) {
|
||||
docs {
|
||||
group {
|
||||
relatedPosts(
|
||||
sort: "createdAt",
|
||||
limit: 0
|
||||
) {
|
||||
docs {
|
||||
title
|
||||
}
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
const unlimited = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query: queryUnlimited }) })
|
||||
.then((res) => res.json())
|
||||
|
||||
expect(pageWithLimit.data.Categories.docs[0].group.relatedPosts.docs).toHaveLength(4)
|
||||
expect(pageWithLimit.data.Categories.docs[0].group.relatedPosts.docs[0].title).toStrictEqual(
|
||||
'test 0',
|
||||
)
|
||||
expect(pageWithLimit.data.Categories.docs[0].group.relatedPosts.hasNextPage).toStrictEqual(
|
||||
true,
|
||||
)
|
||||
|
||||
expect(unlimited.data.Categories.docs[0].group.relatedPosts.docs).toHaveLength(15)
|
||||
expect(unlimited.data.Categories.docs[0].group.relatedPosts.docs[0].title).toStrictEqual(
|
||||
'test 0',
|
||||
)
|
||||
expect(unlimited.data.Categories.docs[0].group.relatedPosts.hasNextPage).toStrictEqual(false)
|
||||
})
|
||||
|
||||
it('should sort joins', async () => {
|
||||
const query = `query {
|
||||
Category(id: ${categoryID}) {
|
||||
relatedPosts(
|
||||
sort: "-title"
|
||||
) {
|
||||
docs {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
const response = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
||||
.then((res) => res.json())
|
||||
expect(response.data.Category.relatedPosts.docs[0].title).toStrictEqual('test 9')
|
||||
})
|
||||
|
||||
it('should query in on collections with joins', async () => {
|
||||
const query = `query {
|
||||
Category(id: ${categoryID}) {
|
||||
relatedPosts(
|
||||
where: {
|
||||
title: {
|
||||
equals: "test 3"
|
||||
}
|
||||
}
|
||||
) {
|
||||
docs {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
const response = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
||||
.then((res) => res.json())
|
||||
expect(response.data.Category.relatedPosts.docs[0].title).toStrictEqual('test 3')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createPost(overrides?: Partial<Post>) {
|
||||
return payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'test',
|
||||
...overrides,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -11,11 +11,12 @@ export interface Config {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
uploads: Upload;
|
||||
pages: Page;
|
||||
posts: Post;
|
||||
relations: Relation;
|
||||
categories: Category;
|
||||
'localized-posts': LocalizedPost;
|
||||
'localized-categories': LocalizedCategory;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
@@ -23,7 +24,7 @@ export interface Config {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
locale: null;
|
||||
locale: 'en' | 'es';
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
@@ -46,74 +47,62 @@ export interface UserAuthOperations {
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "uploads".
|
||||
*/
|
||||
export interface Upload {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
title: string;
|
||||
title?: string | null;
|
||||
category?: (string | null) | Category;
|
||||
group?: {
|
||||
category?: (string | null) | Category;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relations".
|
||||
* via the `definition` "categories".
|
||||
*/
|
||||
export interface Relation {
|
||||
export interface Category {
|
||||
id: string;
|
||||
hasOne?: (string | null) | Post;
|
||||
hasOnePoly?:
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
} | null);
|
||||
hasMany?: (string | Post)[] | null;
|
||||
hasManyPoly?:
|
||||
| (
|
||||
| {
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
}
|
||||
| {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
upload?: (string | null) | Upload;
|
||||
name?: string | null;
|
||||
relatedPosts?: {
|
||||
docs?: (string | Post)[] | null;
|
||||
hasNextPage?: boolean | null;
|
||||
} | null;
|
||||
group?: {
|
||||
relatedPosts?: {
|
||||
docs?: (string | Post)[] | null;
|
||||
hasNextPage?: boolean | null;
|
||||
} | null;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "localized-posts".
|
||||
*/
|
||||
export interface LocalizedPost {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
category?: (string | null) | LocalizedCategory;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "localized-categories".
|
||||
*/
|
||||
export interface LocalizedCategory {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
relatedPosts?: {
|
||||
docs?: (string | LocalizedPost)[] | null;
|
||||
hasNextPage?: boolean | null;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -134,6 +123,42 @@ export interface User {
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'categories';
|
||||
value: string | Category;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'localized-posts';
|
||||
value: string | LocalizedPost;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'localized-categories';
|
||||
value: string | LocalizedCategory;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null);
|
||||
editedAt?: string | null;
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
1708
test/joins/schema.graphql
Normal file
1708
test/joins/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
65
test/joins/seed.ts
Normal file
65
test/joins/seed.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { seedDB } from '../helpers/seed.js'
|
||||
import { categoriesSlug, collectionSlugs, postsSlug } from './shared.js'
|
||||
|
||||
export const seed = async (_payload) => {
|
||||
await _payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
const category = await _payload.create({
|
||||
collection: categoriesSlug,
|
||||
data: {
|
||||
name: 'example',
|
||||
group: {},
|
||||
},
|
||||
})
|
||||
|
||||
await _payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
category: category.id,
|
||||
group: {
|
||||
category: category.id,
|
||||
},
|
||||
title: 'Test Post 1',
|
||||
},
|
||||
})
|
||||
|
||||
await _payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
category: category.id,
|
||||
group: {
|
||||
category: category.id,
|
||||
},
|
||||
title: 'Test Post 2',
|
||||
},
|
||||
})
|
||||
|
||||
await _payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
category: category.id,
|
||||
group: {
|
||||
category: category.id,
|
||||
},
|
||||
title: 'Test Post 3',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function clearAndSeedEverything(_payload: Payload) {
|
||||
return await seedDB({
|
||||
_payload,
|
||||
collectionSlugs,
|
||||
seedFunction: seed,
|
||||
snapshotKey: 'adminTest',
|
||||
})
|
||||
}
|
||||
14
test/joins/shared.ts
Normal file
14
test/joins/shared.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const categoriesSlug = 'categories'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const localizedPostsSlug = 'localized-posts'
|
||||
|
||||
export const localizedCategoriesSlug = 'localized-categories'
|
||||
|
||||
export const collectionSlugs = [
|
||||
categoriesSlug,
|
||||
postsSlug,
|
||||
localizedPostsSlug,
|
||||
localizedCategoriesSlug,
|
||||
]
|
||||
@@ -43,7 +43,6 @@
|
||||
"@payloadcms/plugin-form-builder": "workspace:*",
|
||||
"@payloadcms/plugin-nested-docs": "workspace:*",
|
||||
"@payloadcms/plugin-redirects": "workspace:*",
|
||||
"@payloadcms/plugin-relationship-object-ids": "workspace:*",
|
||||
"@payloadcms/plugin-search": "workspace:*",
|
||||
"@payloadcms/plugin-sentry": "workspace:*",
|
||||
"@payloadcms/plugin-seo": "workspace:*",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
uploads
|
||||
@@ -1,140 +0,0 @@
|
||||
import { relationshipsAsObjectID } from '@payloadcms/plugin-relationship-object-ids'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: 'uploads',
|
||||
upload: true,
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
slug: 'pages',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'relations',
|
||||
fields: [
|
||||
{
|
||||
name: 'hasOne',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
filterOptions: ({ id }) => ({ id: { not_equals: id } }),
|
||||
},
|
||||
{
|
||||
name: 'hasOnePoly',
|
||||
type: 'relationship',
|
||||
relationTo: ['pages', 'posts'],
|
||||
},
|
||||
{
|
||||
name: 'hasMany',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'hasManyPoly',
|
||||
type: 'relationship',
|
||||
relationTo: ['pages', 'posts'],
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'upload',
|
||||
type: 'upload',
|
||||
relationTo: 'uploads',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
plugins: [relationshipsAsObjectID()],
|
||||
onInit: async (payload) => {
|
||||
if (payload.db.name === 'mongoose') {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
const page = await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: 'page',
|
||||
},
|
||||
})
|
||||
|
||||
const post1 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'post 1',
|
||||
},
|
||||
})
|
||||
|
||||
const post2 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'post 2',
|
||||
},
|
||||
})
|
||||
|
||||
const upload = await payload.create({
|
||||
collection: 'uploads',
|
||||
data: {},
|
||||
filePath: path.resolve(dirname, './payload-logo.png'),
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'relations',
|
||||
depth: 0,
|
||||
data: {
|
||||
hasOne: post1.id,
|
||||
hasOnePoly: { relationTo: 'pages', value: page.id },
|
||||
hasMany: [post1.id, post2.id],
|
||||
hasManyPoly: [
|
||||
{ relationTo: 'posts', value: post1.id },
|
||||
{ relationTo: 'pages', value: page.id },
|
||||
],
|
||||
upload: upload.id,
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'relations',
|
||||
depth: 0,
|
||||
data: {
|
||||
hasOnePoly: { relationTo: 'pages', value: page.id },
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
@@ -1,119 +0,0 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { Post, Relation } from './payload-types.js'
|
||||
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
describe('Relationship Object IDs Plugin', () => {
|
||||
let relations: Relation[]
|
||||
let posts: Post[]
|
||||
let payload: Payload
|
||||
|
||||
beforeAll(async () => {
|
||||
;({ payload } = await initPayloadInt(dirname))
|
||||
})
|
||||
|
||||
it('seeds data accordingly', async () => {
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name === 'mongoose') {
|
||||
const relationsQuery = await payload.find({
|
||||
collection: 'relations',
|
||||
sort: 'createdAt',
|
||||
})
|
||||
|
||||
relations = relationsQuery.docs
|
||||
|
||||
const postsQuery = await payload.find({
|
||||
collection: 'posts',
|
||||
sort: 'createdAt',
|
||||
})
|
||||
|
||||
posts = postsQuery.docs
|
||||
|
||||
expect(relationsQuery.totalDocs).toStrictEqual(2)
|
||||
expect(postsQuery.totalDocs).toStrictEqual(2)
|
||||
}
|
||||
})
|
||||
|
||||
it('stores relations as object ids', async () => {
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name === 'mongoose') {
|
||||
const docs = await payload.db.collections.relations.find()
|
||||
expect(typeof docs[0].hasOne).toBe('object')
|
||||
expect(typeof docs[0].hasOnePoly.value).toBe('object')
|
||||
expect(typeof docs[0].hasMany[0]).toBe('object')
|
||||
expect(typeof docs[0].hasManyPoly[0].value).toBe('object')
|
||||
expect(typeof docs[0].upload).toBe('object')
|
||||
}
|
||||
})
|
||||
|
||||
it('can query by relationship id', async () => {
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name === 'mongoose') {
|
||||
const { totalDocs } = await payload.find({
|
||||
collection: 'relations',
|
||||
where: {
|
||||
hasOne: {
|
||||
equals: posts[0].id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(totalDocs).toStrictEqual(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('populates relations', () => {
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name === 'mongoose') {
|
||||
const populatedPostTitle =
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
typeof relations[0].hasOne === 'object' ? relations[0].hasOne.title : undefined
|
||||
expect(populatedPostTitle).toBeDefined()
|
||||
|
||||
const populatedUploadFilename =
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
typeof relations[0].upload === 'object' ? relations[0].upload.filename : undefined
|
||||
|
||||
expect(populatedUploadFilename).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('can query by nested property', async () => {
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name === 'mongoose') {
|
||||
const { totalDocs } = await payload.find({
|
||||
collection: 'relations',
|
||||
where: {
|
||||
'hasOne.title': {
|
||||
equals: 'post 1',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(totalDocs).toStrictEqual(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('can query using the "in" operator', async () => {
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name === 'mongoose') {
|
||||
const { totalDocs } = await payload.find({
|
||||
collection: 'relations',
|
||||
where: {
|
||||
hasMany: {
|
||||
in: [posts[0].id],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(totalDocs).toStrictEqual(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -25,7 +25,6 @@ export const tgzToPkgNameMap = {
|
||||
'@payloadcms/plugin-form-builder': 'payloadcms-plugin-form-builder-*',
|
||||
'@payloadcms/plugin-nested-docs': 'payloadcms-plugin-nested-docs-*',
|
||||
'@payloadcms/plugin-redirects': 'payloadcms-plugin-redirects-*',
|
||||
'@payloadcms/plugin-relationship-object-ids': 'payloadcms-plugin-relationship-object-ids-*',
|
||||
'@payloadcms/plugin-search': 'payloadcms-plugin-search-*',
|
||||
'@payloadcms/plugin-sentry': 'payloadcms-plugin-sentry-*',
|
||||
'@payloadcms/plugin-seo': 'payloadcms-plugin-seo-*',
|
||||
|
||||
Reference in New Issue
Block a user