chore(plugin-multi-tenant): test suite enhancements (#10732)

### What?
Updates test suite multi-tenant config as a better example.

### Why?
So it is easier to follow and write logical tests for in the future.
This commit is contained in:
Jarrod Flesch
2025-01-22 16:28:18 -05:00
committed by GitHub
parent be2c482054
commit 9a8769967c
15 changed files with 241 additions and 233 deletions

View File

@@ -1,4 +1,4 @@
import type { Field, FilterOptionsProps, RelationshipField, Where } from 'payload' import type { Field, FilterOptionsProps, RelationshipField } from 'payload'
import { getTenantFromCookie } from './getTenantFromCookie.js' import { getTenantFromCookie } from './getTenantFromCookie.js'
@@ -7,6 +7,7 @@ type AddFilterOptionsToFieldsArgs = {
tenantEnabledCollectionSlugs: string[] tenantEnabledCollectionSlugs: string[]
tenantEnabledGlobalSlugs: string[] tenantEnabledGlobalSlugs: string[]
} }
export function addFilterOptionsToFields({ export function addFilterOptionsToFields({
fields, fields,
tenantEnabledCollectionSlugs, tenantEnabledCollectionSlugs,

View File

@@ -1,18 +0,0 @@
import type { CollectionConfig } from 'payload'
export const LinksCollection: CollectionConfig = {
slug: 'links',
admin: {
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'url',
type: 'text',
},
],
}

View File

@@ -0,0 +1,43 @@
import type { CollectionConfig } from 'payload'
import { menuSlug } from '../shared.js'
export const Menu: CollectionConfig = {
slug: menuSlug,
admin: {
useAsTitle: 'title',
},
fields: [
{
name: 'title',
label: 'Title',
type: 'text',
required: true,
},
{
name: 'description',
type: 'textarea',
},
{
name: 'menuItems',
label: 'Menu Items',
type: 'array',
fields: [
{
name: 'menuItem',
label: 'Menu Item',
type: 'relationship',
relationTo: 'menu-items',
required: true,
admin: {
description: 'Automatically filtered by selected tenant',
},
},
{
name: 'active',
type: 'checkbox',
},
],
},
],
}

View File

@@ -0,0 +1,17 @@
import type { CollectionConfig } from 'payload'
import { menuItemsSlug } from '../shared.js'
export const MenuItems: CollectionConfig = {
slug: menuItemsSlug,
admin: {
useAsTitle: 'name',
},
fields: [
{
name: 'name',
type: 'text',
required: true,
},
],
}

View File

@@ -1,34 +0,0 @@
import type { CollectionConfig } from 'payload'
import { postsSlug } from '../shared.js'
import { userFilterOptions } from './Users/filterOptions.js'
export const Posts: CollectionConfig = {
slug: postsSlug,
labels: {
singular: 'Post',
plural: 'Posts',
},
admin: {
useAsTitle: 'title',
},
fields: [
{
name: 'title',
label: 'Title',
type: 'text',
required: true,
},
{
name: 'relatedLinks',
relationTo: 'links',
type: 'relationship',
},
{
name: 'author',
relationTo: 'users',
type: 'relationship',
filterOptions: userFilterOptions,
},
],
}

View File

@@ -18,15 +18,16 @@ export const Tenants: CollectionConfig = {
type: 'text', type: 'text',
required: true, required: true,
}, },
{
name: 'slug',
type: 'text',
required: true,
},
{ {
name: 'domain', name: 'domain',
type: 'text', type: 'text',
required: true, required: true,
}, },
{
type: 'join',
name: 'users',
collection: 'users',
on: 'tenants.tenant',
},
], ],
} }

View File

@@ -1,34 +0,0 @@
import type { CollectionConfig } from 'payload'
import { usersSlug } from '../shared.js'
export const Users: CollectionConfig = {
slug: usersSlug,
auth: true,
admin: {
useAsTitle: 'email',
},
access: {
read: () => true,
},
fields: [
// Email added by default
// Add more fields as needed
{
type: 'select',
name: 'roles',
hasMany: true,
options: [
{
label: 'Admin',
value: 'admin',
},
{
label: 'User',
value: 'user',
},
],
saveToJWT: true,
},
],
}

View File

@@ -1,4 +1,4 @@
import type { FilterOptions, Where } from 'payload' import type { FilterOptions } from 'payload'
import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities' import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities'
@@ -9,17 +9,8 @@ export const userFilterOptions: FilterOptions = ({ req }) => {
} }
return { return {
or: [ 'tenants.tenant': {
{ equals: selectedTenant,
'tenants.tenant': { },
equals: selectedTenant, }
},
},
{
roles: {
in: ['admin'],
},
},
],
} as Where
} }

View File

@@ -2,6 +2,7 @@
border-radius: 100%; border-radius: 100%;
height: 18px; height: 18px;
width: 18px; width: 18px;
background-color: var(--theme-error-300);
} }
[data-selected-tenant-title="Blue Dog"] #tenant-icon { [data-selected-tenant-title="Blue Dog"] #tenant-icon {

View File

@@ -2,6 +2,7 @@
border-radius: 100%; border-radius: 100%;
height: 18px; height: 18px;
width: 18px; width: 18px;
background-color: var(--theme-error-300);
} }
[data-selected-tenant-title="Blue Dog"] #tenant-logo { [data-selected-tenant-title="Blue Dog"] #tenant-logo {

View File

@@ -7,15 +7,14 @@ const dirname = path.dirname(filename)
import type { Config as ConfigType } from './payload-types.js' import type { Config as ConfigType } from './payload-types.js'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { LinksCollection } from './collections/Links.js' import { Menu } from './collections/Menu.js'
import { Posts } from './collections/Posts.js' import { MenuItems } from './collections/MenuItems.js'
import { Tenants } from './collections/Tenants.js' import { Tenants } from './collections/Tenants.js'
import { Users } from './collections/Users.js' import { Users } from './collections/Users/index.js'
import { NavigationGlobalCollection } from './globals/Navigation.js'
import { seed } from './seed/index.js' import { seed } from './seed/index.js'
export default buildConfigWithDefaults({ export default buildConfigWithDefaults({
collections: [Users, Tenants, Posts, LinksCollection, NavigationGlobalCollection], collections: [Tenants, Users, Menu, MenuItems],
admin: { admin: {
importMap: { importMap: {
baseDir: path.resolve(dirname), baseDir: path.resolve(dirname),
@@ -47,9 +46,8 @@ export default buildConfigWithDefaults({
access: {}, access: {},
}, },
collections: { collections: {
posts: {}, 'menu-items': {},
links: {}, menu: {
'navigation-global': {
isGlobal: true, isGlobal: true,
}, },
}, },

View File

@@ -1,14 +0,0 @@
import type { CollectionConfig } from 'payload'
export const NavigationGlobalCollection: CollectionConfig = {
slug: 'navigation-global',
admin: {
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
},
],
}

View File

@@ -11,22 +11,24 @@ export interface Config {
users: UserAuthOperations; users: UserAuthOperations;
}; };
collections: { collections: {
users: User;
tenants: Tenant; tenants: Tenant;
posts: Post; users: User;
links: Link; menu: Menu;
'navigation-global': NavigationGlobal; 'menu-items': MenuItem;
'payload-locked-documents': PayloadLockedDocument; 'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference; 'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration; 'payload-migrations': PayloadMigration;
}; };
collectionsJoins: {}; collectionsJoins: {
tenants: {
users: 'users';
};
};
collectionsSelect: { collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
tenants: TenantsSelect<false> | TenantsSelect<true>; tenants: TenantsSelect<false> | TenantsSelect<true>;
posts: PostsSelect<false> | PostsSelect<true>; users: UsersSelect<false> | UsersSelect<true>;
links: LinksSelect<false> | LinksSelect<true>; menu: MenuSelect<false> | MenuSelect<true>;
'navigation-global': NavigationGlobalSelect<false> | NavigationGlobalSelect<true>; 'menu-items': MenuItemsSelect<false> | MenuItemsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>; 'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>; 'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>; 'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -63,6 +65,21 @@ export interface UserAuthOperations {
password: string; password: string;
}; };
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tenants".
*/
export interface Tenant {
id: string;
name: string;
domain: string;
users?: {
docs?: (string | User)[] | null;
hasNextPage?: boolean | null;
} | null;
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users". * via the `definition` "users".
@@ -90,49 +107,34 @@ export interface User {
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tenants". * via the `definition` "menu".
*/ */
export interface Tenant { export interface Menu {
id: string;
name: string;
slug: string;
domain: string;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts".
*/
export interface Post {
id: string; id: string;
tenant?: (string | null) | Tenant; tenant?: (string | null) | Tenant;
title: string; title: string;
relatedLinks?: (string | null) | Link; description?: string | null;
author?: (string | null) | User; menuItems?:
| {
/**
* Automatically filtered by selected tenant
*/
menuItem: string | MenuItem;
active?: boolean | null;
id?: string | null;
}[]
| null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "links". * via the `definition` "menu-items".
*/ */
export interface Link { export interface MenuItem {
id: string; id: string;
tenant?: (string | null) | Tenant; tenant?: (string | null) | Tenant;
title?: string | null; name: string;
url?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "navigation-global".
*/
export interface NavigationGlobal {
id: string;
tenant?: (string | null) | Tenant;
title?: string | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@@ -143,25 +145,21 @@ export interface NavigationGlobal {
export interface PayloadLockedDocument { export interface PayloadLockedDocument {
id: string; id: string;
document?: document?:
| ({
relationTo: 'users';
value: string | User;
} | null)
| ({ | ({
relationTo: 'tenants'; relationTo: 'tenants';
value: string | Tenant; value: string | Tenant;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'users';
value: string | Post; value: string | User;
} | null) } | null)
| ({ | ({
relationTo: 'links'; relationTo: 'menu';
value: string | Link; value: string | Menu;
} | null) } | null)
| ({ | ({
relationTo: 'navigation-global'; relationTo: 'menu-items';
value: string | NavigationGlobal; value: string | MenuItem;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
@@ -205,6 +203,17 @@ export interface PayloadMigration {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tenants_select".
*/
export interface TenantsSelect<T extends boolean = true> {
name?: T;
domain?: T;
users?: T;
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select". * via the `definition` "users_select".
@@ -230,45 +239,29 @@ export interface UsersSelect<T extends boolean = true> {
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tenants_select". * via the `definition` "menu_select".
*/ */
export interface TenantsSelect<T extends boolean = true> { export interface MenuSelect<T extends boolean = true> {
tenant?: T;
title?: T;
description?: T;
menuItems?:
| T
| {
menuItem?: T;
active?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "menu-items_select".
*/
export interface MenuItemsSelect<T extends boolean = true> {
tenant?: T;
name?: T; name?: T;
slug?: T;
domain?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts_select".
*/
export interface PostsSelect<T extends boolean = true> {
tenant?: T;
title?: T;
relatedLinks?: T;
author?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "links_select".
*/
export interface LinksSelect<T extends boolean = true> {
tenant?: T;
title?: T;
url?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "navigation-global_select".
*/
export interface NavigationGlobalSelect<T extends boolean = true> {
tenant?: T;
title?: T;
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
@@ -316,4 +309,4 @@ export interface Auth {
declare module 'payload' { declare module 'payload' {
// @ts-ignore // @ts-ignore
export interface GeneratedTypes extends Config {} export interface GeneratedTypes extends Config {}
} }

View File

@@ -1,39 +1,67 @@
import type { Config } from 'payload' import type { Config } from 'payload'
import { devUser, regularUser } from '../../credentials.js' import { devUser } from '../../credentials.js'
export const seed: Config['onInit'] = async (payload) => { export const seed: Config['onInit'] = async (payload) => {
// create tenants // create tenants
const tenant1 = await payload.create({ const blueDogTenant = await payload.create({
collection: 'tenants', collection: 'tenants',
data: { data: {
name: 'Blue Dog', name: 'Blue Dog',
slug: 'blue-dog',
domain: 'bluedog.com', domain: 'bluedog.com',
}, },
}) })
const tenant2 = await payload.create({ const steelCatTenant = await payload.create({
collection: 'tenants', collection: 'tenants',
data: { data: {
name: 'Steel Cat', name: 'Steel Cat',
slug: 'steel-cat',
domain: 'steelcat.com', domain: 'steelcat.com',
}, },
}) })
// create posts // Create blue dog menu items
await payload.create({ await payload.create({
collection: 'posts', collection: 'menu-items',
data: { data: {
title: 'Blue Dog Post', name: 'Chorizo Con Queso and Chips',
tenant: tenant1.id, tenant: blueDogTenant.id,
}, },
}) })
await payload.create({ await payload.create({
collection: 'posts', collection: 'menu-items',
data: { data: {
title: 'Steel Cat Post', name: 'Garlic Parmesan Tots',
tenant: tenant2.id, tenant: blueDogTenant.id,
},
})
await payload.create({
collection: 'menu-items',
data: {
name: 'Spicy Mac',
tenant: blueDogTenant.id,
},
})
// Create steel cat menu items
await payload.create({
collection: 'menu-items',
data: {
name: 'Pretzel Bites',
tenant: steelCatTenant.id,
},
})
await payload.create({
collection: 'menu-items',
data: {
name: 'Buffalo Chicken Dip',
tenant: steelCatTenant.id,
},
})
await payload.create({
collection: 'menu-items',
data: {
name: 'Pulled Pork Nachos',
tenant: steelCatTenant.id,
}, },
}) })
@@ -50,12 +78,44 @@ export const seed: Config['onInit'] = async (payload) => {
await payload.create({ await payload.create({
collection: 'users', collection: 'users',
data: { data: {
email: regularUser.email, email: 'jane@blue-dog.com',
password: regularUser.password, password: 'test',
roles: ['user'], roles: ['user'],
tenants: [ tenants: [
{ {
tenant: tenant1.id, tenant: blueDogTenant.id,
},
],
},
})
// create menus
await payload.create({
collection: 'menu',
data: {
description: 'This collection behaves like globals, 1 document per tenant. No list view.',
title: 'Blue Dog Menu',
tenant: blueDogTenant.id,
},
})
await payload.create({
collection: 'menu',
data: {
description: 'This collection behaves like globals, 1 document per tenant. No list view.',
title: 'Steel Cat Menu',
tenant: steelCatTenant.id,
},
})
await payload.create({
collection: 'users',
data: {
email: 'huel@steel-cat.com',
password: 'test',
roles: ['user'],
tenants: [
{
tenant: steelCatTenant.id,
}, },
], ],
}, },

View File

@@ -1,5 +1,7 @@
export const tenantsSlug = 'tenants' export const tenantsSlug = 'tenants'
export const postsSlug = 'posts'
export const usersSlug = 'users' export const usersSlug = 'users'
export const menuItemsSlug = 'menu-items'
export const menuSlug = 'menu'