Files
payload-feature-flags/src/index.ts

235 lines
6.0 KiB
TypeScript

import type { Config, CollectionConfig, Field } from 'payload'
import { customEndpointHandler } from './endpoints/customEndpointHandler.js'
export type CollectionOverrides = Partial<
Omit<CollectionConfig, 'fields'>
> & {
fields?: (args: { defaultFields: Field[] }) => Field[]
}
export type PayloadFeatureFlagsConfig = {
/**
* Enable/disable the plugin
* @default false
*/
disabled?: boolean
/**
* Default value for new feature flags
* @default false
*/
defaultValue?: boolean
/**
* Enable percentage-based rollouts
* @default true
*/
enableRollouts?: boolean
/**
* Enable variant/experiment support (A/B testing)
* @default true
*/
enableVariants?: boolean
/**
* Enable REST API endpoints for feature flags
* @default false
*/
enableApi?: boolean
/**
* Override collection configuration
*/
collectionOverrides?: CollectionOverrides
}
export const payloadFeatureFlags =
(pluginOptions: PayloadFeatureFlagsConfig = {}) =>
(config: Config): Config => {
const {
disabled = false,
defaultValue = false,
enableRollouts = true,
enableVariants = true,
enableApi = false,
collectionOverrides = {},
} = pluginOptions
// Get collection slug from overrides or use default
const collectionSlug = collectionOverrides.slug || 'feature-flags'
if (!config.collections) {
config.collections = []
}
// Define default fields
const defaultFields: Field[] = [
{
name: 'name',
type: 'text',
required: true,
unique: true,
admin: {
description: 'Unique identifier for the feature flag',
},
},
{
name: 'description',
type: 'textarea',
admin: {
description: 'Describe what this feature flag controls',
},
},
{
name: 'enabled',
type: 'checkbox',
defaultValue: defaultValue,
required: true,
admin: {
description: 'Toggle this feature flag on or off',
},
},
...(enableRollouts ? [
{
name: 'rolloutPercentage',
type: 'number',
min: 0,
max: 100,
defaultValue: 100,
admin: {
description: 'Percentage of users who will see this feature (0-100)',
condition: (data: any) => data?.enabled === true,
},
},
] : []),
...(enableVariants ? [
{
name: 'variants',
type: 'array',
admin: {
description: 'Define variants for A/B testing',
condition: (data: any) => data?.enabled === true,
},
fields: [
{
name: 'name',
type: 'text',
required: true,
admin: {
description: 'Variant identifier (e.g., control, variant-a)',
},
},
{
name: 'weight',
type: 'number',
min: 0,
max: 100,
required: true,
admin: {
description: 'Weight for this variant (all weights should sum to 100)',
},
},
{
name: 'metadata',
type: 'json',
admin: {
description: 'Additional data for this variant',
},
},
],
},
] : []),
{
name: 'tags',
type: 'array',
fields: [
{
name: 'tag',
type: 'text',
},
],
admin: {
description: 'Tags for organizing feature flags',
},
},
{
name: 'metadata',
type: 'json',
admin: {
description: 'Additional metadata for this feature flag',
},
},
]
// Apply field overrides if provided
const fields = collectionOverrides.fields
? collectionOverrides.fields({ defaultFields })
: defaultFields
// Extract field overrides from collectionOverrides
const { fields: _fieldsOverride, ...otherOverrides } = collectionOverrides
// Create the feature flags collection with overrides
const featureFlagsCollection: CollectionConfig = {
slug: collectionSlug,
admin: {
useAsTitle: 'name',
group: 'Configuration',
description: 'Manage feature flags for your application',
...(otherOverrides.admin || {}),
},
fields,
// Apply any other collection overrides
...otherOverrides,
}
config.collections.push(featureFlagsCollection)
/**
* If the plugin is disabled, we still want to keep the collection
* so the database schema is consistent which is important for migrations.
*/
if (disabled) {
return config
}
if (!config.endpoints) {
config.endpoints = []
}
if (!config.admin) {
config.admin = {}
}
if (!config.admin.components) {
config.admin.components = {}
}
if (!config.admin.components.beforeDashboard) {
config.admin.components.beforeDashboard = []
}
config.admin.components.beforeDashboard.push(
`payload-feature-flags/client#BeforeDashboardClient`,
)
config.admin.components.beforeDashboard.push(
`payload-feature-flags/rsc#BeforeDashboardServer`,
)
// Add API endpoints if enabled
if (enableApi) {
// Add API endpoint for fetching feature flags
config.endpoints.push({
handler: customEndpointHandler(collectionSlug),
method: 'get',
path: '/feature-flags',
})
// Add endpoint for checking a specific feature flag
config.endpoints.push({
handler: customEndpointHandler(collectionSlug),
method: 'get',
path: '/feature-flags/:flag',
})
}
return config
}