Skip to content

Commit 564fdb0

Browse files
r1tsuuDanRibbens
andauthored
fix: virtual relationship fields with select (#12266)
Continuation of #12265. Currently, using `select` on new relationship virtual fields: ``` const doc = await payload.findByID({ collection: 'virtual-relations', depth: 0, id, select: { postTitle: true }, }) ``` doesn't work, because in order to calculate `post.title`, the `post` field must be selected as well. This PR adds logic that sanitizes the incoming `select` to include those relationships into `select` (that are related to selected virtual fields) --------- Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
1 parent 47a1eee commit 564fdb0

File tree

21 files changed

+268
-4
lines changed

21 files changed

+268
-4
lines changed

packages/payload/src/collections/operations/create.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ export const createOperation = async <
247247
let doc
248248

249249
const select = sanitizeSelect({
250+
fields: collectionConfig.flattenedFields,
250251
forceSelect: collectionConfig.forceSelect,
251252
select: incomingSelect,
252253
})

packages/payload/src/collections/operations/delete.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export const deleteOperation = async <
110110
const fullWhere = combineQueries(where, accessResult)
111111

112112
const select = sanitizeSelect({
113+
fields: collectionConfig.flattenedFields,
113114
forceSelect: collectionConfig.forceSelect,
114115
select: incomingSelect,
115116
})

packages/payload/src/collections/operations/deleteByID.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
168168
}
169169

170170
const select = sanitizeSelect({
171+
fields: collectionConfig.flattenedFields,
171172
forceSelect: collectionConfig.forceSelect,
172173
select: incomingSelect,
173174
})

packages/payload/src/collections/operations/find.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export const findOperation = async <
102102
} = args
103103

104104
const select = sanitizeSelect({
105+
fields: collectionConfig.flattenedFields,
105106
forceSelect: collectionConfig.forceSelect,
106107
select: incomingSelect,
107108
})

packages/payload/src/collections/operations/findByID.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export const findByIDOperation = async <
8787
} = args
8888

8989
const select = sanitizeSelect({
90+
fields: collectionConfig.flattenedFields,
9091
forceSelect: collectionConfig.forceSelect,
9192
select: incomingSelect,
9293
})

packages/payload/src/collections/operations/findVersionByID.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { APIError, Forbidden, NotFound } from '../../errors/index.js'
1111
import { afterRead } from '../../fields/hooks/afterRead/index.js'
1212
import { killTransaction } from '../../utilities/killTransaction.js'
1313
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
14+
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js'
1415
import { getQueryDraftsSelect } from '../../versions/drafts/getQueryDraftsSelect.js'
1516

1617
export type Arguments = {
@@ -70,8 +71,10 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
7071
// /////////////////////////////////////
7172

7273
const select = sanitizeSelect({
74+
fields: buildVersionCollectionFields(payload.config, collectionConfig, true),
7375
forceSelect: getQueryDraftsSelect({ select: collectionConfig.forceSelect }),
7476
select: incomingSelect,
77+
versions: true,
7578
})
7679

7780
const versionsQuery = await payload.db.findVersions<TData>({

packages/payload/src/collections/operations/findVersions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
7272
const fullWhere = combineQueries(where, accessResults)
7373

7474
const select = sanitizeSelect({
75+
fields: buildVersionCollectionFields(payload.config, collectionConfig, true),
7576
forceSelect: getQueryDraftsSelect({ select: collectionConfig.forceSelect }),
7677
select: incomingSelect,
78+
versions: true,
7779
})
7880

7981
// /////////////////////////////////////

packages/payload/src/collections/operations/restoreVersion.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
117117
// /////////////////////////////////////
118118

119119
const select = sanitizeSelect({
120+
fields: collectionConfig.flattenedFields,
120121
forceSelect: collectionConfig.forceSelect,
121122
select: incomingSelect,
122123
})

packages/payload/src/collections/operations/update.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ export const updateOperation = async <
201201

202202
try {
203203
const select = sanitizeSelect({
204+
fields: collectionConfig.flattenedFields,
204205
forceSelect: collectionConfig.forceSelect,
205206
select: incomingSelect,
206207
})

packages/payload/src/collections/operations/updateByID.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export const updateByIDOperation = async <
161161
})
162162

163163
const select = sanitizeSelect({
164+
fields: collectionConfig.flattenedFields,
164165
forceSelect: collectionConfig.forceSelect,
165166
select: incomingSelect,
166167
})

packages/payload/src/globals/operations/findOne.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
5353
}
5454

5555
const select = sanitizeSelect({
56+
fields: globalConfig.flattenedFields,
5657
forceSelect: globalConfig.forceSelect,
5758
select: incomingSelect,
5859
})

packages/payload/src/globals/operations/findVersionByID.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { afterRead } from '../../fields/hooks/afterRead/index.js'
1111
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
1212
import { killTransaction } from '../../utilities/killTransaction.js'
1313
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
14+
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js'
15+
import { buildVersionGlobalFields } from '../../versions/buildGlobalFields.js'
1416
import { getQueryDraftsSelect } from '../../versions/drafts/getQueryDraftsSelect.js'
1517

1618
export type Arguments = {
@@ -60,8 +62,10 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
6062
const hasWhereAccess = typeof accessResults === 'object'
6163

6264
const select = sanitizeSelect({
65+
fields: buildVersionGlobalFields(payload.config, globalConfig, true),
6366
forceSelect: getQueryDraftsSelect({ select: globalConfig.forceSelect }),
6467
select: incomingSelect,
68+
versions: true,
6569
})
6670

6771
const findGlobalVersionsArgs: FindGlobalVersionsArgs = {

packages/payload/src/globals/operations/findVersions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
7070
const fullWhere = combineQueries(where, accessResults)
7171

7272
const select = sanitizeSelect({
73+
fields: buildVersionGlobalFields(payload.config, globalConfig, true),
7374
forceSelect: getQueryDraftsSelect({ select: globalConfig.forceSelect }),
7475
select: incomingSelect,
76+
versions: true,
7577
})
7678

7779
// /////////////////////////////////////

packages/payload/src/globals/operations/update.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ export const updateOperation = async <
246246
// /////////////////////////////////////
247247

248248
const select = sanitizeSelect({
249+
fields: globalConfig.flattenedFields,
249250
forceSelect: globalConfig.forceSelect,
250251
select: incomingSelect,
251252
})
Lines changed: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,129 @@
11
import { deepMergeSimple } from '@payloadcms/translations/utilities'
22

3-
import type { SelectType } from '../types/index.js'
3+
import type { FlattenedField } from '../fields/config/types.js'
4+
import type { SelectIncludeType, SelectType } from '../types/index.js'
45

56
import { getSelectMode } from './getSelectMode.js'
67

8+
// Transform post.title -> post, post.category.title -> post
9+
const stripVirtualPathToCurrentCollection = ({
10+
fields,
11+
path,
12+
versions,
13+
}: {
14+
fields: FlattenedField[]
15+
path: string
16+
versions: boolean
17+
}) => {
18+
const resultSegments: string[] = []
19+
20+
if (versions) {
21+
resultSegments.push('version')
22+
const versionField = fields.find((each) => each.name === 'version')
23+
24+
if (versionField && versionField.type === 'group') {
25+
fields = versionField.flattenedFields
26+
}
27+
}
28+
29+
for (const segment of path.split('.')) {
30+
const field = fields.find((each) => each.name === segment)
31+
32+
if (!field) {
33+
continue
34+
}
35+
36+
resultSegments.push(segment)
37+
38+
if (field.type === 'relationship' || field.type === 'upload') {
39+
return resultSegments.join('.')
40+
}
41+
}
42+
43+
return resultSegments.join('.')
44+
}
45+
46+
const getAllVirtualRelations = ({ fields }: { fields: FlattenedField[] }) => {
47+
const result: string[] = []
48+
49+
for (const field of fields) {
50+
if ('virtual' in field && typeof field.virtual === 'string') {
51+
result.push(field.virtual)
52+
} else if (field.type === 'group' || field.type === 'tab') {
53+
const nestedResult = getAllVirtualRelations({ fields: field.flattenedFields })
54+
55+
for (const nestedItem of nestedResult) {
56+
result.push(nestedItem)
57+
}
58+
}
59+
}
60+
61+
return result
62+
}
63+
64+
const resolveVirtualRelationsToSelect = ({
65+
fields,
66+
selectValue,
67+
topLevelFields,
68+
versions,
69+
}: {
70+
fields: FlattenedField[]
71+
selectValue: SelectIncludeType | true
72+
topLevelFields: FlattenedField[]
73+
versions: boolean
74+
}) => {
75+
const result: string[] = []
76+
if (selectValue === true) {
77+
for (const item of getAllVirtualRelations({ fields })) {
78+
result.push(
79+
stripVirtualPathToCurrentCollection({ fields: topLevelFields, path: item, versions }),
80+
)
81+
}
82+
} else {
83+
for (const fieldName in selectValue) {
84+
const field = fields.find((each) => each.name === fieldName)
85+
if (!field) {
86+
continue
87+
}
88+
89+
if ('virtual' in field && typeof field.virtual === 'string') {
90+
result.push(
91+
stripVirtualPathToCurrentCollection({
92+
fields: topLevelFields,
93+
path: field.virtual,
94+
versions,
95+
}),
96+
)
97+
} else if (field.type === 'group' || field.type === 'tab') {
98+
for (const item of resolveVirtualRelationsToSelect({
99+
fields: field.flattenedFields,
100+
selectValue: selectValue[fieldName],
101+
topLevelFields,
102+
versions,
103+
})) {
104+
result.push(
105+
stripVirtualPathToCurrentCollection({ fields: topLevelFields, path: item, versions }),
106+
)
107+
}
108+
}
109+
}
110+
}
111+
112+
return result
113+
}
114+
7115
export const sanitizeSelect = ({
116+
fields,
8117
forceSelect,
9118
select,
119+
versions,
10120
}: {
121+
fields: FlattenedField[]
11122
forceSelect?: SelectType
12123
select?: SelectType
124+
versions?: boolean
13125
}): SelectType | undefined => {
14-
if (!forceSelect || !select) {
126+
if (!select) {
15127
return select
16128
}
17129

@@ -21,5 +133,36 @@ export const sanitizeSelect = ({
21133
return select
22134
}
23135

24-
return deepMergeSimple(select, forceSelect)
136+
if (forceSelect) {
137+
select = deepMergeSimple(select, forceSelect)
138+
}
139+
140+
if (select) {
141+
const virtualRelations = resolveVirtualRelationsToSelect({
142+
fields,
143+
selectValue: select as SelectIncludeType,
144+
topLevelFields: fields,
145+
versions: versions ?? false,
146+
})
147+
148+
for (const path of virtualRelations) {
149+
let currentRef = select
150+
const segments = path.split('.')
151+
for (let i = 0; i < segments.length; i++) {
152+
const isLast = segments.length - 1 === i
153+
const segment = segments[i]
154+
155+
if (isLast) {
156+
currentRef[segment] = true
157+
} else {
158+
if (!(segment in currentRef)) {
159+
currentRef[segment] = {}
160+
currentRef = currentRef[segment]
161+
}
162+
}
163+
}
164+
}
165+
}
166+
167+
return select
25168
}

test/database/int.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,6 +1997,23 @@ describe('database', () => {
19971997
expect(draft.docs[0]?.postTitle).toBe('my-title')
19981998
})
19991999

2000+
it('should not break when using select', async () => {
2001+
const post = await payload.create({ collection: 'posts', data: { title: 'my-title-10' } })
2002+
const { id } = await payload.create({
2003+
collection: 'virtual-relations',
2004+
depth: 0,
2005+
data: { post: post.id },
2006+
})
2007+
2008+
const doc = await payload.findByID({
2009+
collection: 'virtual-relations',
2010+
depth: 0,
2011+
id,
2012+
select: { postTitle: true },
2013+
})
2014+
expect(doc.postTitle).toBe('my-title-10')
2015+
})
2016+
20002017
it('should allow virtual field as reference to ID', async () => {
20012018
const post = await payload.create({ collection: 'posts', data: { title: 'my-title' } })
20022019
const { id } = await payload.create({
@@ -2129,6 +2146,26 @@ describe('database', () => {
21292146
expect(doc.postCategoryTitle).toBe('1-category')
21302147
})
21312148

2149+
it('should not break when using select 2x deep', async () => {
2150+
const category = await payload.create({
2151+
collection: 'categories',
2152+
data: { title: '3-category' },
2153+
})
2154+
const post = await payload.create({
2155+
collection: 'posts',
2156+
data: { title: '3-post', category: category.id },
2157+
})
2158+
const doc = await payload.create({ collection: 'virtual-relations', data: { post: post.id } })
2159+
2160+
const docWithSelect = await payload.findByID({
2161+
collection: 'virtual-relations',
2162+
depth: 0,
2163+
id: doc.id,
2164+
select: { postCategoryTitle: true },
2165+
})
2166+
expect(docWithSelect.postCategoryTitle).toBe('3-category')
2167+
})
2168+
21322169
it('should allow to query by virtual field 2x deep', async () => {
21332170
const category = await payload.create({
21342171
collection: 'categories',

test/plugin-import-export/collections/Pages.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ export const Pages: CollectionConfig = {
9898
type: 'relationship',
9999
relationTo: 'users',
100100
},
101+
{
102+
name: 'virtualRelationship',
103+
type: 'text',
104+
virtual: 'author.name',
105+
},
106+
{
107+
name: 'virtual',
108+
type: 'text',
109+
virtual: true,
110+
hooks: {
111+
afterRead: [() => 'virtual value'],
112+
},
113+
},
101114
{
102115
name: 'hasManyNumber',
103116
type: 'number',

test/plugin-import-export/collections/Users.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export const Users: CollectionConfig = {
1010
read: () => true,
1111
},
1212
fields: [
13+
{
14+
name: 'name',
15+
type: 'text',
16+
},
1317
// Email added by default
1418
// Add more fields as needed
1519
],

0 commit comments

Comments
 (0)