Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/payload/src/fields/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,11 @@ export type FilterOptionsProps<TData = any> = {
user: Partial<PayloadRequest['user']>
}

export type FilterOptionsFunc<TData = any> = (
export type FilterOptionsFunction<TData = any> = (
options: FilterOptionsProps<TData>,
) => boolean | Promise<boolean | Where> | Where

export type FilterOptions<TData = any> = FilterOptionsFunc<TData> | null | Where
export type FilterOptions<TData = any> = FilterOptionsFunction<TData> | null | Where

type BlockSlugOrString = (({} & string) | BlockSlug)[]

Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,7 @@ export type {
FieldWithSubFields,
FieldWithSubFieldsClient,
FilterOptions,
FilterOptionsFunction,
FilterOptionsProps,
FlattenedArrayField,
FlattenedBlock,
Expand Down
27 changes: 27 additions & 0 deletions packages/plugin-multi-tenant/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
ArrayField,
CollectionSlug,
Field,
FilterOptionsFunction,
RelationshipField,
SingleRelationshipField,
TypedUser,
Expand Down Expand Up @@ -246,3 +247,29 @@ export type UserWithTenantsField = {
}[]
| null
} & TypedUser

declare module 'payload' {
export interface FieldCustom {
'plugin-multi-tenant'?: {
/**
* Function to override the filterOptions result for the relationship field.
* The original filterOptions / filterOptions injected by the multi-tenant plugin will still run,
* and the result will be passed as the first argument to this function.
*
* This allows you to combine your own filtering logic with the multi-tenant filtering logic.
*
* While passing your own `filterOptions` function to the field config will allow you to add stricter filter options
* in addition to the multi-tenant filtering, this property allows you to override the result of the multi-tenant filtering entirely
* for cases where you need to make it less strict.
*
* @param args.filterOptionsResult - The result of the original filterOptions function
* @param args.filterOptionsArgs - The arguments that were passed to the original filterOptions function
* ```ts
*/
filterOptionsOverride?: (args: {
filterOptionsArgs: Parameters<FilterOptionsFunction>[0]
filterOptionsResult: Awaited<ReturnType<FilterOptionsFunction>>
}) => ReturnType<FilterOptionsFunction>
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Block, Config, Field, RelationshipField, SanitizedConfig, TypedUser } from 'payload'
import type { Block, Config, Field, RelationshipField, SanitizedConfig } from 'payload'

import type { MultiTenantPluginConfig } from '../types.js'

Expand Down Expand Up @@ -170,42 +170,54 @@ function addFilter<ConfigType = unknown>({
// User specified filter
const originalFilter = field.filterOptions
field.filterOptions = async (args) => {
const originalFilterResult =
typeof originalFilter === 'function' ? await originalFilter(args) : (originalFilter ?? true)
const getResult = async () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff from line 173 - 211 is just wrapping the original code inside getResult.

const originalFilterResult =
typeof originalFilter === 'function' ? await originalFilter(args) : (originalFilter ?? true)

// If the relationTo is not a tenant enabled collection, return early
if (args.relationTo && !tenantEnabledCollectionSlugs.includes(args.relationTo)) {
return originalFilterResult
}
// If the relationTo is not a tenant enabled collection, return early
if (args.relationTo && !tenantEnabledCollectionSlugs.includes(args.relationTo)) {
return originalFilterResult
}

// If the original filtr returns false, return early
if (originalFilterResult === false) {
return false
}
// If the original filter returns false, return early
if (originalFilterResult === false) {
return false
}

// Custom tenant filter
const tenantFilterResults = filterDocumentsByTenants({
docTenantID: args.data?.[tenantFieldName],
filterFieldName: tenantFieldName,
req: args.req,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
tenantsCollectionSlug,
userHasAccessToAllTenants,
})

// If the tenant filter returns null, meaning no tenant filter, just use the original filter
if (tenantFilterResults === null) {
return originalFilterResult
}
// Custom tenant filter
const tenantFilterResults = filterDocumentsByTenants({
docTenantID: args.data?.[tenantFieldName],
filterFieldName: tenantFieldName,
req: args.req,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
tenantsCollectionSlug,
userHasAccessToAllTenants,
})

// If the tenant filter returns null, meaning no tenant filter, just use the original filter
if (tenantFilterResults === null) {
return originalFilterResult
}

// If the original filter returns true, just use the tenant filter
if (originalFilterResult === true) {
return tenantFilterResults
}

// If the original filter returns true, just use the tenant filter
if (originalFilterResult === true) {
return tenantFilterResults
return {
and: [originalFilterResult, tenantFilterResults],
}
}
let result = await getResult()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New functionality is implemented starting here


return {
and: [originalFilterResult, tenantFilterResults],
if (field?.custom?.['plugin-multi-tenant']?.filterOptionsOverride) {
result = await field.custom['plugin-multi-tenant'].filterOptionsOverride({
filterOptionsArgs: args,
filterOptionsResult: result,
})
}

return result
}
}
23 changes: 22 additions & 1 deletion test/plugin-multi-tenant/collections/Relationships.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CollectionConfig } from 'payload'
import type { CollectionConfig, Where } from 'payload'

export const Relationships: CollectionConfig = {
slug: 'relationships',
Expand All @@ -16,6 +16,27 @@ export const Relationships: CollectionConfig = {
name: 'relationship',
type: 'relationship',
relationTo: 'relationships',
custom: {
'plugin-multi-tenant': {
filterOptionsOverride({ filterOptionsResult }) {
if (typeof filterOptionsResult === 'object' && filterOptionsResult !== null) {
// Wrap
const newResult: Where = {
or: [
filterOptionsResult,
{
'tenant.isPublic': {
equals: true,
},
},
],
}
return newResult
}
return filterOptionsResult
},
},
},
},
],
}
27 changes: 27 additions & 0 deletions test/plugin-multi-tenant/int.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('@payloadcms/plugin-multi-tenant', () => {
let blueDogRelationships: PaginatedDocs<Relationship>
let anchorBarTenantID: DefaultDocumentIDType
let blueDogTenantID: DefaultDocumentIDType
let publicRelationships: PaginatedDocs<Relationship>

beforeEach(async () => {
anchorBarRelationships = await payload.find({
Expand All @@ -74,6 +75,14 @@ describe('@payloadcms/plugin-multi-tenant', () => {
},
},
})
publicRelationships = await payload.find({
collection: 'relationships',
where: {
'tenant.name': {
equals: 'Public Tenant',
},
},
})

// @ts-expect-error unsafe access okay in test

Expand Down Expand Up @@ -132,6 +141,24 @@ describe('@payloadcms/plugin-multi-tenant', () => {
}),
).rejects.toThrow('The following field is invalid: Relationship')
})

it('ensure relationship document with relationship within different tenant can be created if the document is allowed via custom filterOptions override', async () => {
const newRelationship = await payload.create({
collection: 'relationships',
data: {
title: 'Relationship to Anchor Bar',
// @ts-expect-error unsafe access okay in test
relationship: publicRelationships.docs[0].id,
tenant: anchorBarTenantID,
},
req: {
headers: new Headers([['cookie', `payload-tenant=${anchorBarTenantID}`]]),
},
})

// @ts-expect-error unsafe access okay in test
expect(newRelationship.relationship?.title).toBe('Owned by public tenant')
})
})
})
})