feat: adds multi-tenant plugin (#10447)
### Multi Tenant Plugin
This PR adds a `@payloadcms/plugin-multi-tenant` package. The goal is to
consolidate a source of truth for multi-tenancy. Currently we are
maintaining different implementations for clients, users in discord and
our examples repo. When updates or new paradigms arise we need to
communicate this with everyone and update code examples which is hard to
maintain.
### What does it do?
- adds a tenant selector to the sidebar, above the nav links
- adds a hidden tenant field to every collection that you specify
- adds an array field to your users collection, allowing you to assign
users to tenants
- by default combines the access control (to enabled collections) that
you define, with access control based on the tenants assigned to user on
the request
- by default adds a baseListFilter that filters the documents shown in
the list view with the selected tenant in the admin panel
### What does it not do?
- it does not implement multi-tenancy for your frontend. You will need
to query data for specific tenants to build your website/application
- it does not add a tenants collection, you **NEED** to add a tenants
collection, where you can define what types of fields you would like on
it
### The plugin config
Most of the options listed below are _optional_, but it is easier to
just lay out all of the configuration options.
**TS Type**
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
*
* @default true
*/
cleanupAfterTenantDelete?: boolean
/**
* Automatically
*/
collections: {
[key in CollectionSlug]?: {
/**
* Set to `true` if you want the collection to behave as a global
*
* @default false
*/
isGlobal?: boolean
/**
* Set to `false` if you want to manually apply the baseListFilter
*
* @default true
*/
useBaseListFilter?: boolean
/**
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
*
* @default true
*/
useTenantAccess?: boolean
}
}
/**
* Enables debug mode
* - Makes the tenant field visible in the admin UI within applicable collections
*
* @default false
*/
debug?: boolean
/**
* Enables the multi-tenant plugin
*
* @default true
*/
enabled?: boolean
/**
* Field configuration for the field added to all tenant enabled collections
*/
tenantField?: {
access?: RelationshipField['access']
/**
* The name of the field added to all tenant enabled collections
*
* @default 'tenant'
*/
name?: string
}
/**
* Field configuration for the field added to the users collection
*
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
* This is useful if you want to customize the field or place the field in a specific location
*/
tenantsArrayField?:
| {
/**
* Access configuration for the array field
*/
arrayFieldAccess?: ArrayField['access']
/**
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
*/
includeDefaultField?: true
/**
* Additional fields to include on the tenants array field
*/
rowFields?: Field[]
/**
* Access configuration for the tenant field
*/
tenantFieldAccess?: RelationshipField['access']
}
| {
arrayFieldAccess?: never
/**
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
*/
includeDefaultField?: false
rowFields?: never
tenantFieldAccess?: never
}
/**
* The slug for the tenant collection
*
* @default 'tenants'
*/
tenantsSlug?: string
/**
* Function that determines if a user has access to _all_ tenants
*
* Useful for super-admin type users
*/
userHasAccessToAllTenants?: (
user: ConfigTypes extends { user: User } ? ConfigTypes['user'] : User,
) => boolean
}
```
**Example usage**
```ts
import type { Config } from './payload-types'
import { buildConfig } from 'payload'
export default buildConfig({
plugins: [
multiTenantPlugin<Config>({
collections: {
pages: {},
},
userHasAccessToAllTenants: (user) => isSuperAdmin(user),
}),
],
})
```
### How to configure Collections as Globals for multi-tenant
When using multi-tenant, globals need to actually be configured as
collections so the content can be specific per tenant.
To do that, you can mark a collection with `isGlobal` and it will behave
like a global and users will not see the list view.
```ts
multiTenantPlugin({
collections: {
navigation: {
isGlobal: true,
},
},
})
```
This commit is contained in:
18
test/plugin-multi-tenant/collections/Links.ts
Normal file
18
test/plugin-multi-tenant/collections/Links.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const LinksCollection: CollectionConfig = {
|
||||
slug: 'links',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
37
test/plugin-multi-tenant/collections/Posts.ts
Normal file
37
test/plugin-multi-tenant/collections/Posts.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { postsSlug } from '../shared.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: 'excerpt',
|
||||
label: 'Excerpt',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'slug',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'relatedLinks',
|
||||
relationTo: 'links',
|
||||
type: 'relationship',
|
||||
},
|
||||
],
|
||||
}
|
||||
32
test/plugin-multi-tenant/collections/Tenants.ts
Normal file
32
test/plugin-multi-tenant/collections/Tenants.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { tenantsSlug } from '../shared.js'
|
||||
|
||||
export const Tenants: CollectionConfig = {
|
||||
slug: tenantsSlug,
|
||||
labels: {
|
||||
singular: 'Tenant',
|
||||
plural: 'Tenants',
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'domain',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
34
test/plugin-multi-tenant/collections/Users.ts
Normal file
34
test/plugin-multi-tenant/collections/Users.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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,
|
||||
},
|
||||
],
|
||||
}
|
||||
55
test/plugin-multi-tenant/config.ts
Normal file
55
test/plugin-multi-tenant/config.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
import type { Config as ConfigType } from './payload-types.js'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { LinksCollection } from './collections/Links.js'
|
||||
import { Posts } from './collections/Posts.js'
|
||||
import { Tenants } from './collections/Tenants.js'
|
||||
import { Users } from './collections/Users.js'
|
||||
import { NavigationGlobalCollection } from './globals/Navigation.js'
|
||||
import { seed } from './seed/index.js'
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [Users, Tenants, Posts, LinksCollection, NavigationGlobalCollection],
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
onInit: seed,
|
||||
plugins: [
|
||||
multiTenantPlugin<ConfigType>({
|
||||
userHasAccessToAllTenants: (user) => Boolean(user.roles?.includes('admin')),
|
||||
tenantsArrayField: {
|
||||
rowFields: [
|
||||
{
|
||||
name: 'roles',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Admin', value: 'admin' },
|
||||
{ label: 'User', value: 'user' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
tenantField: {
|
||||
access: {},
|
||||
},
|
||||
collections: {
|
||||
posts: {},
|
||||
links: {},
|
||||
'navigation-global': {
|
||||
isGlobal: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
19
test/plugin-multi-tenant/eslint.config.js
Normal file
19
test/plugin-multi-tenant/eslint.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { rootParserOptions } from '../../eslint.config.js'
|
||||
import testEslintConfig from '../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...testEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
...rootParserOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
14
test/plugin-multi-tenant/globals/Navigation.ts
Normal file
14
test/plugin-multi-tenant/globals/Navigation.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const NavigationGlobalCollection: CollectionConfig = {
|
||||
slug: 'navigation-global',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
54
test/plugin-multi-tenant/int.spec.ts
Normal file
54
test/plugin-multi-tenant/int.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import path from 'path'
|
||||
import { NotFound, type Payload } from 'payload'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
|
||||
let payload: Payload
|
||||
let restClient: NextRESTClient
|
||||
let token: string
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
describe('@payloadcms/plugin-multi-tenant', () => {
|
||||
beforeAll(async () => {
|
||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
||||
|
||||
const data = await restClient
|
||||
.POST('/users/login', {
|
||||
body: JSON.stringify({
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
token = data.token
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (typeof payload.db.destroy === 'function') {
|
||||
await payload.db.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
describe('tenants', () => {
|
||||
it('should create a tenant', async () => {
|
||||
const tenant1 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'tenant1',
|
||||
domain: 'tenant1.com',
|
||||
slug: 'tenant1',
|
||||
},
|
||||
})
|
||||
|
||||
expect(tenant1).toHaveProperty('id')
|
||||
})
|
||||
})
|
||||
})
|
||||
321
test/plugin-multi-tenant/payload-types.ts
Normal file
321
test/plugin-multi-tenant/payload-types.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
users: User;
|
||||
tenants: Tenant;
|
||||
posts: Post;
|
||||
links: Link;
|
||||
'navigation-global': NavigationGlobal;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
tenants: TenantsSelect<false> | TenantsSelect<true>;
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
links: LinksSelect<false> | LinksSelect<true>;
|
||||
'navigation-global': NavigationGlobalSelect<false> | NavigationGlobalSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
roles?: ('admin' | 'user')[] | null;
|
||||
tenants?:
|
||||
| {
|
||||
tenant: string | Tenant;
|
||||
roles?: ('admin' | 'user') | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "tenants".
|
||||
*/
|
||||
export interface Tenant {
|
||||
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;
|
||||
tenant?: (string | null) | Tenant;
|
||||
title: string;
|
||||
excerpt?: string | null;
|
||||
slug?: string | null;
|
||||
relatedLinks?: (string | null) | Link;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "links".
|
||||
*/
|
||||
export interface Link {
|
||||
id: string;
|
||||
tenant?: (string | null) | Tenant;
|
||||
title?: string | null;
|
||||
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;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'tenants';
|
||||
value: string | Tenant;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'links';
|
||||
value: string | Link;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'navigation-global';
|
||||
value: string | NavigationGlobal;
|
||||
} | 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".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
roles?: T;
|
||||
tenants?:
|
||||
| T
|
||||
| {
|
||||
tenant?: T;
|
||||
roles?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "tenants_select".
|
||||
*/
|
||||
export interface TenantsSelect<T extends boolean = true> {
|
||||
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;
|
||||
excerpt?: T;
|
||||
slug?: T;
|
||||
relatedLinks?: 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;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
63
test/plugin-multi-tenant/seed/index.ts
Normal file
63
test/plugin-multi-tenant/seed/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { Config } from 'payload'
|
||||
|
||||
import { devUser, regularUser } from '../../credentials.js'
|
||||
|
||||
export const seed: Config['onInit'] = async (payload) => {
|
||||
// create tenants
|
||||
const tenant1 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Blue Dog',
|
||||
slug: 'blue-dog',
|
||||
domain: 'bluedog.com',
|
||||
},
|
||||
})
|
||||
const tenant2 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Steel Cat',
|
||||
slug: 'steel-cat',
|
||||
domain: 'steelcat.com',
|
||||
},
|
||||
})
|
||||
|
||||
// create posts
|
||||
await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Blue Dog Post',
|
||||
tenant: tenant1.id,
|
||||
},
|
||||
})
|
||||
await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Steel Cat Post',
|
||||
tenant: tenant2.id,
|
||||
},
|
||||
})
|
||||
|
||||
// create users
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
roles: ['admin'],
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: regularUser.email,
|
||||
password: regularUser.password,
|
||||
roles: ['user'],
|
||||
tenants: [
|
||||
{
|
||||
tenant: tenant1.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
5
test/plugin-multi-tenant/shared.ts
Normal file
5
test/plugin-multi-tenant/shared.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const tenantsSlug = 'tenants'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const usersSlug = 'users'
|
||||
13
test/plugin-multi-tenant/tsconfig.eslint.json
Normal file
13
test/plugin-multi-tenant/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// extend your base config to share compilerOptions, etc
|
||||
//"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// ensure that nobody can accidentally use this config for a build
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
// whatever paths you intend to lint
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
]
|
||||
}
|
||||
3
test/plugin-multi-tenant/tsconfig.json
Normal file
3
test/plugin-multi-tenant/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.json"
|
||||
}
|
||||
Reference in New Issue
Block a user