UNPKG

@payloadcms/plugin-multi-tenant

Version:
421 lines (420 loc) 20.2 kB
import { defaults } from './defaults.js'; import { getTenantOptionsEndpoint } from './endpoints/getTenantOptionsEndpoint.js'; import { tenantField } from './fields/tenantField/index.js'; import { tenantsArrayField } from './fields/tenantsArrayField/index.js'; import { filterDocumentsByTenants } from './filters/filterDocumentsByTenants.js'; import { addTenantCleanup } from './hooks/afterTenantDelete.js'; import { translations } from './translations/index.js'; import { addCollectionAccess } from './utilities/addCollectionAccess.js'; import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js'; import { combineFilters } from './utilities/combineFilters.js'; import { miniChalk } from './utilities/miniChalk.js'; export const multiTenantPlugin = (pluginConfig)=>(incomingConfig)=>{ if (pluginConfig.enabled === false) { return incomingConfig; } /** * Set defaults */ const userHasAccessToAllTenants = typeof pluginConfig.userHasAccessToAllTenants === 'function' ? pluginConfig.userHasAccessToAllTenants : ()=>false; const tenantsCollectionSlug = pluginConfig.tenantsSlug = pluginConfig.tenantsSlug || defaults.tenantCollectionSlug; const tenantFieldName = pluginConfig?.tenantField?.name || defaults.tenantFieldName; const tenantsArrayFieldName = pluginConfig?.tenantsArrayField?.arrayFieldName || defaults.tenantsArrayFieldName; const tenantsArrayTenantFieldName = pluginConfig?.tenantsArrayField?.arrayTenantFieldName || defaults.tenantsArrayTenantFieldName; const basePath = pluginConfig.basePath || defaults.basePath; /** * Add defaults for admin properties */ if (!incomingConfig.admin) { incomingConfig.admin = {}; } if (!incomingConfig.admin?.components) { incomingConfig.admin.components = { actions: [], beforeNavLinks: [], providers: [] }; } if (!incomingConfig.admin.components?.providers) { incomingConfig.admin.components.providers = []; } if (!incomingConfig.admin.components?.actions) { incomingConfig.admin.components.actions = []; } if (!incomingConfig.admin.components?.beforeNavLinks) { incomingConfig.admin.components.beforeNavLinks = []; } if (!incomingConfig.collections) { incomingConfig.collections = []; } /** * Add tenants array field to users collection */ const adminUsersCollection = incomingConfig.collections.find(({ slug, auth })=>{ if (incomingConfig.admin?.user) { return slug === incomingConfig.admin.user; } else if (auth) { return true; } }); if (!adminUsersCollection) { throw Error('An auth enabled collection was not found'); } /** * Add tenants array field to users collection */ if (pluginConfig?.tenantsArrayField?.includeDefaultField !== false) { adminUsersCollection.fields.push(tenantsArrayField({ ...pluginConfig?.tenantsArrayField || {}, tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug })); } addCollectionAccess({ accessResultCallback: pluginConfig.usersAccessResultOverride, adminUsersSlug: adminUsersCollection.slug, collection: adminUsersCollection, fieldName: `${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`, tenantsArrayFieldName, tenantsArrayTenantFieldName, userHasAccessToAllTenants }); if (pluginConfig.useUsersTenantFilter !== false) { if (!adminUsersCollection.admin) { adminUsersCollection.admin = {}; } const baseFilter = adminUsersCollection.admin?.baseFilter ?? adminUsersCollection.admin?.baseListFilter; adminUsersCollection.admin.baseFilter = combineFilters({ baseFilter, customFilter: (args)=>filterDocumentsByTenants({ filterFieldName: `${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`, req: args.req, tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug, userHasAccessToAllTenants }) }); } let tenantCollection; const [collectionSlugs, globalCollectionSlugs] = Object.keys(pluginConfig.collections).reduce((acc, slug)=>{ if (pluginConfig?.collections?.[slug]?.isGlobal) { acc[1].push(slug); } else { acc[0].push(slug); } return acc; }, [ [], [] ]); // used to track and not duplicate filterOptions on referenced blocks const blockReferencesWithFilters = []; // used to validate enabled collection slugs const multiTenantCollectionsFound = []; /** * The folders collection is added AFTER the plugin is initialized * so if they added the folder slug to the plugin collections, * we can assume that they have folders enabled */ const foldersSlug = incomingConfig.folders ? incomingConfig.folders.slug || 'payload-folders' : 'payload-folders'; if (collectionSlugs.includes(foldersSlug)) { multiTenantCollectionsFound.push(foldersSlug); incomingConfig.folders = incomingConfig.folders || {}; incomingConfig.folders.collectionOverrides = incomingConfig.folders.collectionOverrides || []; incomingConfig.folders.collectionOverrides.push(({ collection })=>{ /** * Add filter options to all relationship fields */ collection.fields = addFilterOptionsToFields({ blockReferencesWithFilters, config: incomingConfig, fields: collection.fields, tenantEnabledCollectionSlugs: collectionSlugs, tenantEnabledGlobalSlugs: globalCollectionSlugs, tenantFieldName, tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug, userHasAccessToAllTenants }); if (pluginConfig.collections[foldersSlug]?.customTenantField !== true) { /** * Add tenant field to enabled collections */ collection.fields.unshift(tenantField({ name: tenantFieldName, debug: pluginConfig.debug, overrides: pluginConfig.collections[collection.slug]?.tenantFieldOverrides ? pluginConfig.collections[collection.slug]?.tenantFieldOverrides : pluginConfig.tenantField || {}, tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug, unique: false })); } const { useBaseFilter, useBaseListFilter } = pluginConfig.collections[collection.slug] || {}; if (useBaseFilter ?? useBaseListFilter ?? true) { /** * Add list filter to enabled collections * - filters results by selected tenant */ collection.admin = collection.admin || {}; collection.admin.baseFilter = combineFilters({ baseFilter: collection.admin?.baseFilter ?? collection.admin?.baseListFilter, customFilter: (args)=>filterDocumentsByTenants({ filterFieldName: tenantFieldName, req: args.req, tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug, userHasAccessToAllTenants }) }); } if (pluginConfig.collections[foldersSlug]?.useTenantAccess !== false) { /** * Add access control constraint to tenant enabled folders collection */ addCollectionAccess({ accessResultCallback: pluginConfig.collections[foldersSlug]?.accessResultOverride, adminUsersSlug: adminUsersCollection.slug, collection, fieldName: tenantFieldName, tenantsArrayFieldName, tenantsArrayTenantFieldName, userHasAccessToAllTenants }); } return collection; }); } /** * Modify collections */ incomingConfig.collections.forEach((collection)=>{ /** * Modify tenants collection */ if (collection.slug === tenantsCollectionSlug) { tenantCollection = collection; if (pluginConfig.useTenantsCollectionAccess !== false) { /** * Add access control constraint to tenants collection * - constrains access a users assigned tenants */ addCollectionAccess({ adminUsersSlug: adminUsersCollection.slug, collection, fieldName: 'id', tenantsArrayFieldName, tenantsArrayTenantFieldName, userHasAccessToAllTenants }); } if (pluginConfig.useTenantsListFilter !== false) { /** * Add list filter to tenants collection * - filter by selected tenant */ if (!collection.admin) { collection.admin = {}; } const baseFilter = collection.admin?.baseFilter ?? collection.admin?.baseListFilter; collection.admin.baseFilter = combineFilters({ baseFilter, customFilter: (args)=>filterDocumentsByTenants({ filterFieldName: 'id', req: args.req, tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug, userHasAccessToAllTenants }) }); } if (pluginConfig.cleanupAfterTenantDelete !== false) { /** * Add cleanup logic when tenant is deleted * - delete documents related to tenant * - remove tenant from users */ addTenantCleanup({ collection, enabledSlugs: [ ...collectionSlugs, ...globalCollectionSlugs ], tenantFieldName, tenantsCollectionSlug, usersSlug: adminUsersCollection.slug, usersTenantsArrayFieldName: tenantsArrayFieldName, usersTenantsArrayTenantFieldName: tenantsArrayTenantFieldName }); } /** * Add custom tenant field that watches and dispatches updates to the selector */ collection.fields.push({ name: '_watchTenant', type: 'ui', admin: { components: { Field: { path: '@payloadcms/plugin-multi-tenant/client#WatchTenantCollection' } }, disableBulkEdit: true, disableListColumn: true } }); collection.endpoints = [ ...collection.endpoints || [], getTenantOptionsEndpoint({ tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug, useAsTitle: tenantCollection.admin?.useAsTitle || 'id', userHasAccessToAllTenants }) ]; } else if (pluginConfig.collections?.[collection.slug]) { multiTenantCollectionsFound.push(collection.slug); const isGlobal = Boolean(pluginConfig.collections[collection.slug]?.isGlobal); if (isGlobal) { collection.disableDuplicate = true; } if (!pluginConfig.debug && !isGlobal) { collection.admin ??= {}; collection.admin.components ??= {}; collection.admin.components.edit ??= {}; collection.admin.components.edit.editMenuItems ??= []; collection.admin.components.edit.editMenuItems.push({ path: '@payloadcms/plugin-multi-tenant/client#AssignTenantFieldTrigger' }); } /** * Add filter options to all relationship fields */ collection.fields = addFilterOptionsToFields({ blockReferencesWithFilters, config: incomingConfig, fields: collection.fields, tenantEnabledCollectionSlugs: collectionSlugs, tenantEnabledGlobalSlugs: globalCollectionSlugs, tenantFieldName, tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug, userHasAccessToAllTenants }); if (pluginConfig.collections[collection.slug]?.customTenantField !== true) { /** * Add tenant field to enabled collections */ collection.fields.unshift(tenantField({ name: tenantFieldName, debug: pluginConfig.debug, overrides: pluginConfig.collections[collection.slug]?.tenantFieldOverrides ? pluginConfig.collections[collection.slug]?.tenantFieldOverrides : pluginConfig.tenantField || {}, tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug, unique: isGlobal })); } const { useBaseFilter, useBaseListFilter } = pluginConfig.collections[collection.slug] || {}; if (useBaseFilter ?? useBaseListFilter ?? true) { /** * Add list filter to enabled collections * - filters results by selected tenant */ collection.admin = collection.admin || {}; collection.admin.baseFilter = combineFilters({ baseFilter: collection.admin?.baseFilter ?? collection.admin?.baseListFilter, customFilter: (args)=>filterDocumentsByTenants({ filterFieldName: tenantFieldName, req: args.req, tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug, userHasAccessToAllTenants }) }); } if (pluginConfig.collections[collection.slug]?.useTenantAccess !== false) { /** * Add access control constraint to tenant enabled collection */ addCollectionAccess({ accessResultCallback: pluginConfig.collections[collection.slug]?.accessResultOverride, adminUsersSlug: adminUsersCollection.slug, collection, fieldName: tenantFieldName, tenantsArrayFieldName, tenantsArrayTenantFieldName, userHasAccessToAllTenants }); } } }); if (!tenantCollection) { throw new Error(`Tenants collection not found with slug: ${tenantsCollectionSlug}`); } if (multiTenantCollectionsFound.length !== collectionSlugs.length + globalCollectionSlugs.length) { const missingSlugs = [ ...collectionSlugs, ...globalCollectionSlugs ].filter((slug)=>!multiTenantCollectionsFound.includes(slug)); // eslint-disable-next-line no-console console.error(miniChalk.yellowBold('WARNING (plugin-multi-tenant)'), 'missing collections', missingSlugs, 'try placing the multi-tenant plugin after other plugins.'); } /** * Add TenantSelectionProvider to admin providers */ incomingConfig.admin.components.providers.push({ clientProps: { tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug: tenantCollection.slug, useAsTitle: tenantCollection.admin?.useAsTitle || 'id', userHasAccessToAllTenants }, path: '@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider' }); /** * Add global redirect action */ if (globalCollectionSlugs.length) { incomingConfig.admin.components.actions.push({ path: '@payloadcms/plugin-multi-tenant/rsc#GlobalViewRedirect', serverProps: { basePath, globalSlugs: globalCollectionSlugs, tenantFieldName, tenantsArrayFieldName, tenantsArrayTenantFieldName, tenantsCollectionSlug, useAsTitle: tenantCollection.admin?.useAsTitle || 'id', userHasAccessToAllTenants } }); } /** * Add tenant selector to admin UI */ incomingConfig.admin.components.beforeNavLinks.push({ clientProps: { enabledSlugs: [ ...collectionSlugs, ...globalCollectionSlugs, adminUsersCollection.slug, tenantCollection.slug ], label: pluginConfig.tenantSelectorLabel || undefined }, path: '@payloadcms/plugin-multi-tenant/rsc#TenantSelector' }); /** * Merge plugin translations */ if (!incomingConfig.i18n) { incomingConfig.i18n = {}; } Object.entries(translations).forEach(([locale, pluginI18nObject])=>{ const typedLocale = locale; if (!incomingConfig.i18n.translations) { incomingConfig.i18n.translations = {}; } if (!(typedLocale in incomingConfig.i18n.translations)) { incomingConfig.i18n.translations[typedLocale] = {}; } if (!('plugin-multi-tenant' in incomingConfig.i18n.translations[typedLocale])) { ; incomingConfig.i18n.translations[typedLocale]['plugin-multi-tenant'] = {}; } ; incomingConfig.i18n.translations[typedLocale]['plugin-multi-tenant'] = { ...pluginI18nObject.translations['plugin-multi-tenant'], ...pluginConfig.i18n?.translations?.[typedLocale] || {} }; }); return incomingConfig; }; //# sourceMappingURL=index.js.map