Skip to content

Commit 98b6791

Browse files
fix(plugin-multi-tenant): relationTo arrays inflating filterOptions where query size (#14944)
Before the plugin was looping over relationTo arrays and would build up duplicate tenant queries. Now it just ensures that there is at lease 1 tenant enabled collection and adds a single tenant constraint to the filterOptions. Helpful to note that the relationships are populated 1 at a time and the filterOptions receives `relationTo` arg, the injected filter checks to see if the requested relation is a tenant enabled collection also before applying the constraint.
1 parent 47f63fa commit 98b6791

8 files changed

Lines changed: 237 additions & 36 deletions

File tree

packages/plugin-multi-tenant/src/utilities/addFilterOptionsToFields.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function addFilterOptionsToFields<ConfigType = unknown>({
3636
for (const field of fields) {
3737
let newField: Field = { ...field }
3838
if (newField.type === 'relationship') {
39+
let hasTenantRelationsips = false
3940
/**
4041
* Adjusts relationship fields to filter by tenant
4142
* and ensures relationTo cannot be a tenant global collection
@@ -47,15 +48,7 @@ export function addFilterOptionsToFields<ConfigType = unknown>({
4748
)
4849
}
4950
if (tenantEnabledCollectionSlugs.includes(newField.relationTo)) {
50-
newField = addFilter({
51-
field: newField,
52-
tenantEnabledCollectionSlugs,
53-
tenantFieldName,
54-
tenantsArrayFieldName,
55-
tenantsArrayTenantFieldName,
56-
tenantsCollectionSlug,
57-
userHasAccessToAllTenants,
58-
})
51+
hasTenantRelationsips = true
5952
}
6053
} else {
6154
for (const relationTo of newField.relationTo) {
@@ -65,18 +58,22 @@ export function addFilterOptionsToFields<ConfigType = unknown>({
6558
)
6659
}
6760
if (tenantEnabledCollectionSlugs.includes(relationTo)) {
68-
newField = addFilter({
69-
field: newField as RelationshipField,
70-
tenantEnabledCollectionSlugs,
71-
tenantFieldName,
72-
tenantsArrayFieldName,
73-
tenantsArrayTenantFieldName,
74-
tenantsCollectionSlug,
75-
userHasAccessToAllTenants,
76-
})
61+
hasTenantRelationsips = true
7762
}
7863
}
7964
}
65+
66+
if (hasTenantRelationsips) {
67+
newField = addRelationshipFilter({
68+
field: newField as RelationshipField,
69+
tenantEnabledCollectionSlugs,
70+
tenantFieldName,
71+
tenantsArrayFieldName,
72+
tenantsArrayTenantFieldName,
73+
tenantsCollectionSlug,
74+
userHasAccessToAllTenants,
75+
})
76+
}
8077
}
8178

8279
if (
@@ -175,7 +172,7 @@ type AddFilterArgs<ConfigType = unknown> = {
175172
MultiTenantPluginConfig<ConfigType>
176173
>['userHasAccessToAllTenants']
177174
}
178-
function addFilter<ConfigType = unknown>({
175+
function addRelationshipFilter<ConfigType = unknown>({
179176
field,
180177
tenantEnabledCollectionSlugs,
181178
tenantFieldName,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { Page } from '@playwright/test'
2+
3+
import { expect } from '@playwright/test'
4+
import { wait } from 'payload/shared'
5+
6+
import { selectInput } from '../../selectInput.js'
7+
8+
/**
9+
* Opens a list drawer for a relationship field with appearance="drawer"
10+
* and optionally selects a specific collection type for polymorphic relationships.
11+
*
12+
* @param page - Playwright Page object
13+
* @param fieldName - Name of the relationship field (e.g., 'relationship', 'relationshipHasMany')
14+
* @param selectRelation - Optional: Collection slug to select for polymorphic relationships (e.g., 'posts', 'users')
15+
*
16+
* @example
17+
* // Open list drawer for a non-polymorphic relationship
18+
* await openRelationshipFieldDrawer({ page, fieldName: 'relationship' })
19+
*
20+
* @example
21+
* // Open list drawer and select a specific collection for polymorphic relationship
22+
* await openRelationshipFieldDrawer({
23+
* page,
24+
* fieldName: 'polymorphicRelationship',
25+
* selectRelation: 'tenants'
26+
* })
27+
*/
28+
export async function openRelationshipFieldDrawer({
29+
fieldName,
30+
page,
31+
selectRelation,
32+
}: {
33+
fieldName: string
34+
page: Page
35+
selectRelation?: string
36+
}): Promise<void> {
37+
await wait(300)
38+
39+
// Click the relationship field to open the list drawer
40+
const relationshipField = page.locator(`#field-${fieldName}`)
41+
await relationshipField.click()
42+
43+
// Wait for list drawer to be visible
44+
const listDrawerContent = page.locator('.list-drawer').locator('.drawer__content')
45+
await expect(listDrawerContent).toBeVisible()
46+
47+
// If a specific relation type should be selected (for polymorphic relationships)
48+
if (selectRelation) {
49+
const relationToSelector = page.locator('.list-header__select-collection')
50+
await expect(relationToSelector).toBeVisible()
51+
52+
await selectInput({
53+
selectLocator: relationToSelector,
54+
option: selectRelation,
55+
multiSelect: false,
56+
selectType: 'select',
57+
})
58+
}
59+
}

test/plugin-multi-tenant/collections/MenuItems.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Access, CollectionConfig, Where } from 'payload'
22

33
import { getUserTenantIDs } from '@payloadcms/plugin-multi-tenant/utilities'
44

5-
import { menuItemsSlug } from '../shared.js'
5+
import { menuItemsSlug, notTenantedSlug, relationshipsSlug } from '../shared.js'
66

77
const collectionTenantReadAccess: Access = ({ req }) => {
88
// admins can access all tenants
@@ -97,5 +97,13 @@ export const MenuItems: CollectionConfig = {
9797
name: 'content',
9898
type: 'richText',
9999
},
100+
{
101+
name: 'polymorphicRelationship',
102+
type: 'relationship',
103+
relationTo: [relationshipsSlug, menuItemsSlug, notTenantedSlug],
104+
admin: {
105+
appearance: 'drawer',
106+
},
107+
},
100108
],
101109
}

test/plugin-multi-tenant/collections/Relationships.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { CollectionConfig } from 'payload'
22

3+
import { relationshipsSlug } from '../shared.js'
4+
35
export const Relationships: CollectionConfig = {
4-
slug: 'relationships',
6+
slug: relationshipsSlug,
57
admin: {
68
useAsTitle: 'title',
79
group: 'Tenant Collections',
@@ -15,7 +17,7 @@ export const Relationships: CollectionConfig = {
1517
{
1618
name: 'relationship',
1719
type: 'relationship',
18-
relationTo: 'relationships',
20+
relationTo: relationshipsSlug,
1921
},
2022
],
2123
}

test/plugin-multi-tenant/config.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,29 @@ import { Relationships } from './collections/Relationships.js'
1515
import { Tenants } from './collections/Tenants.js'
1616
import { Users } from './collections/Users/index.js'
1717
import { seed } from './seed/index.js'
18-
import { autosaveGlobalSlug, menuItemsSlug, menuSlug } from './shared.js'
18+
import { autosaveGlobalSlug, menuItemsSlug, menuSlug, notTenantedSlug } from './shared.js'
1919

2020
export default buildConfigWithDefaults({
21-
collections: [Tenants, Users, MenuItems, Menu, AutosaveGlobal, Relationships],
21+
collections: [
22+
Tenants,
23+
Users,
24+
MenuItems,
25+
Menu,
26+
AutosaveGlobal,
27+
Relationships,
28+
{
29+
slug: notTenantedSlug,
30+
admin: {
31+
useAsTitle: 'name',
32+
},
33+
fields: [
34+
{
35+
name: 'name',
36+
type: 'text',
37+
},
38+
],
39+
},
40+
],
2241
admin: {
2342
autoLogin: false,
2443
importMap: {

test/plugin-multi-tenant/e2e.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from '../helpers.js'
1818
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
1919
import { loginClientSide } from '../helpers/e2e/auth/login.js'
20+
import { openRelationshipFieldDrawer } from '../helpers/e2e/fields/relationship/openRelationshipFieldDrawer.js'
2021
import { goToListDoc } from '../helpers/e2e/goToListDoc.js'
2122
import {
2223
clearSelectInput,
@@ -516,6 +517,78 @@ test.describe('Multi Tenant', () => {
516517
})
517518
})
518519

520+
test.describe('Polymorphic Relationships', () => {
521+
test('should not duplicate tenant constraints in polymorphic relationship queries', async () => {
522+
await loginClientSide({
523+
data: credentials.admin,
524+
page,
525+
serverURL,
526+
})
527+
528+
// Capture render-list server action requests
529+
const renderListRequests: Array<{
530+
payload: any[]
531+
url: string
532+
}> = []
533+
534+
page.on('request', (request) => {
535+
// Check for server action POST requests
536+
if (
537+
request.method() === 'POST' &&
538+
request.url().includes(`/admin/collections/${menuItemsSlug}`)
539+
) {
540+
const postData = request.postData()
541+
if (postData) {
542+
try {
543+
const parsedPayload = JSON.parse(postData)
544+
// Check if this is a render-list action
545+
if (Array.isArray(parsedPayload) && parsedPayload[0]?.name === 'render-list') {
546+
renderListRequests.push({
547+
url: request.url(),
548+
payload: parsedPayload,
549+
})
550+
}
551+
} catch (e) {
552+
// Ignore parse errors
553+
}
554+
}
555+
}
556+
})
557+
558+
// Navigate to existing menu item
559+
await page.goto(menuItemsURL.list)
560+
await clearTenantFilter({ page })
561+
562+
await goToListDoc({
563+
cellClass: '.cell-name',
564+
page,
565+
textToMatch: 'Spicy Mac',
566+
urlUtil: menuItemsURL,
567+
})
568+
569+
await openRelationshipFieldDrawer({
570+
page,
571+
fieldName: 'polymorphicRelationship',
572+
selectRelation: 'Relationship', // select a tenant-enabled collection
573+
})
574+
575+
await expect.poll(() => renderListRequests.length).toBeGreaterThan(0)
576+
577+
// Check the query.where clause for tenant constraint duplication
578+
for (const request of renderListRequests) {
579+
const renderListAction = request.payload[0]
580+
await expect.poll(() => renderListAction.name).toBe('render-list')
581+
await expect.poll(() => renderListAction.args).toBeDefined()
582+
await expect.poll(() => renderListAction.args.query).toBeDefined()
583+
584+
const whereString = JSON.stringify(renderListAction.args.query.where)
585+
const tenantMatches = whereString.match(/"tenant":/g)?.length
586+
587+
await expect.poll(() => tenantMatches).toEqual(1)
588+
}
589+
})
590+
})
591+
519592
test.describe('Tenant Selector', () => {
520593
test('should populate tenant selector on login', async () => {
521594
await loginClientSide({

test/plugin-multi-tenant/payload-types.ts

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface Config {
7373
'food-menu': FoodMenu;
7474
'autosave-global': AutosaveGlobal;
7575
relationships: Relationship;
76+
notTenanted: NotTenanted;
7677
'payload-kv': PayloadKv;
7778
'payload-locked-documents': PayloadLockedDocument;
7879
'payload-preferences': PayloadPreference;
@@ -90,6 +91,7 @@ export interface Config {
9091
'food-menu': FoodMenuSelect<false> | FoodMenuSelect<true>;
9192
'autosave-global': AutosaveGlobalSelect<false> | AutosaveGlobalSelect<true>;
9293
relationships: RelationshipsSelect<false> | RelationshipsSelect<true>;
94+
notTenanted: NotTenantedSelect<false> | NotTenantedSelect<true>;
9395
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
9496
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
9597
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -201,6 +203,41 @@ export interface FoodItem {
201203
};
202204
[k: string]: unknown;
203205
} | null;
206+
polymorphicRelationship?:
207+
| ({
208+
relationTo: 'relationships';
209+
value: string | Relationship;
210+
} | null)
211+
| ({
212+
relationTo: 'food-items';
213+
value: string | FoodItem;
214+
} | null)
215+
| ({
216+
relationTo: 'notTenanted';
217+
value: string | NotTenanted;
218+
} | null);
219+
updatedAt: string;
220+
createdAt: string;
221+
}
222+
/**
223+
* This interface was referenced by `Config`'s JSON-Schema
224+
* via the `definition` "relationships".
225+
*/
226+
export interface Relationship {
227+
id: string;
228+
tenant?: (string | null) | Tenant;
229+
title: string;
230+
relationship?: (string | null) | Relationship;
231+
updatedAt: string;
232+
createdAt: string;
233+
}
234+
/**
235+
* This interface was referenced by `Config`'s JSON-Schema
236+
* via the `definition` "notTenanted".
237+
*/
238+
export interface NotTenanted {
239+
id: string;
240+
name?: string | null;
204241
updatedAt: string;
205242
createdAt: string;
206243
}
@@ -239,18 +276,6 @@ export interface AutosaveGlobal {
239276
createdAt: string;
240277
_status?: ('draft' | 'published') | null;
241278
}
242-
/**
243-
* This interface was referenced by `Config`'s JSON-Schema
244-
* via the `definition` "relationships".
245-
*/
246-
export interface Relationship {
247-
id: string;
248-
tenant?: (string | null) | Tenant;
249-
title: string;
250-
relationship?: (string | null) | Relationship;
251-
updatedAt: string;
252-
createdAt: string;
253-
}
254279
/**
255280
* This interface was referenced by `Config`'s JSON-Schema
256281
* via the `definition` "payload-kv".
@@ -298,6 +323,10 @@ export interface PayloadLockedDocument {
298323
| ({
299324
relationTo: 'relationships';
300325
value: string | Relationship;
326+
} | null)
327+
| ({
328+
relationTo: 'notTenanted';
329+
value: string | NotTenanted;
301330
} | null);
302331
globalSlug?: string | null;
303332
user: {
@@ -392,6 +421,7 @@ export interface FoodItemsSelect<T extends boolean = true> {
392421
name?: T;
393422
localizedName?: T;
394423
content?: T;
424+
polymorphicRelationship?: T;
395425
updatedAt?: T;
396426
createdAt?: T;
397427
}
@@ -436,6 +466,15 @@ export interface RelationshipsSelect<T extends boolean = true> {
436466
updatedAt?: T;
437467
createdAt?: T;
438468
}
469+
/**
470+
* This interface was referenced by `Config`'s JSON-Schema
471+
* via the `definition` "notTenanted_select".
472+
*/
473+
export interface NotTenantedSelect<T extends boolean = true> {
474+
name?: T;
475+
updatedAt?: T;
476+
createdAt?: T;
477+
}
439478
/**
440479
* This interface was referenced by `Config`'s JSON-Schema
441480
* via the `definition` "payload-kv_select".

0 commit comments

Comments
 (0)