Bump version to 0.0.6

This commit is contained in:
2025-09-28 22:12:57 +02:00
parent 3c06eba812
commit 42bdb832d0
2 changed files with 38 additions and 37 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xtr-dev/payload-feature-flags", "name": "@xtr-dev/payload-feature-flags",
"version": "0.0.5", "version": "0.0.6",
"description": "Feature flags plugin for Payload CMS - manage feature toggles, A/B tests, and gradual rollouts", "description": "Feature flags plugin for Payload CMS - manage feature toggles, A/B tests, and gradual rollouts",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",

View File

@@ -1,4 +1,5 @@
import { Payload } from 'payload' import { Payload } from 'payload'
import { cache } from "react"
export interface FeatureFlag { export interface FeatureFlag {
name: string name: string
@@ -16,10 +17,10 @@ export interface FeatureFlag {
function getCollectionSlug(payload: Payload): string { function getCollectionSlug(payload: Payload): string {
try { try {
// Look for the feature flags collection - it should have a 'name' field with unique constraint // Look for the feature flags collection - it should have a 'name' field with unique constraint
const collection = payload.config.collections?.find(col => const collection = payload.config.collections?.find(col =>
col.fields.some((field: any) => col.fields.some((field: any) =>
field.name === 'name' && field.name === 'name' &&
field.type === 'text' && field.type === 'text' &&
field.unique === true field.unique === true
) && ) &&
col.fields.some((field: any) => field.name === 'enabled' && field.type === 'checkbox') col.fields.some((field: any) => field.name === 'enabled' && field.type === 'checkbox')
@@ -33,16 +34,16 @@ function getCollectionSlug(payload: Payload): string {
/** /**
* Get a specific feature flag by name (for use in React Server Components) * Get a specific feature flag by name (for use in React Server Components)
*/ */
export async function getFeatureFlag(flagName: string, payload?: Payload): Promise<FeatureFlag | null> { export const getFeatureFlag = cache(async (flagName: string, payload?: Payload): Promise<FeatureFlag | null> => {
try { try {
// If no payload provided, return null as these hooks should be used within Payload context // If no payload provided, return null as these hooks should be used within Payload context
if (!payload) { if (!payload) {
console.error('Payload instance not available. These hooks should be called within Payload server context or pass payload as parameter.') console.error('Payload instance not available. These hooks should be called within Payload server context or pass payload as parameter.')
return null return null
} }
const collectionSlug = getCollectionSlug(payload) const collectionSlug = getCollectionSlug(payload)
const result = await payload.find({ const result = await payload.find({
collection: collectionSlug, collection: collectionSlug,
where: { where: {
@@ -58,7 +59,7 @@ export async function getFeatureFlag(flagName: string, payload?: Payload): Promi
} }
const flag = result.docs[0] const flag = result.docs[0]
return { return {
name: flag.name as string, name: flag.name as string,
enabled: flag.enabled as boolean, enabled: flag.enabled as boolean,
@@ -70,29 +71,29 @@ export async function getFeatureFlag(flagName: string, payload?: Payload): Promi
console.error(`Failed to fetch feature flag ${flagName}:`, error) console.error(`Failed to fetch feature flag ${flagName}:`, error)
return null return null
} }
} })
/** /**
* Check if a feature flag is enabled (for use in React Server Components) * Check if a feature flag is enabled (for use in React Server Components)
*/ */
export async function isFeatureEnabled(flagName: string, payload?: Payload): Promise<boolean> { export const isFeatureEnabled = cache(async (flagName: string, payload?: Payload): Promise<boolean> => {
const flag = await getFeatureFlag(flagName, payload) const flag = await getFeatureFlag(flagName, payload)
return flag?.enabled ?? false return flag?.enabled ?? false
} })
/** /**
* Get all active feature flags (for use in React Server Components) * Get all active feature flags (for use in React Server Components)
*/ */
export async function getAllFeatureFlags(payload?: Payload): Promise<Record<string, FeatureFlag>> { export const getAllFeatureFlags = cache(async (payload?: Payload): Promise<Record<string, FeatureFlag>> => {
try { try {
// If no payload provided, return empty object as these hooks should be used within Payload context // If no payload provided, return empty object as these hooks should be used within Payload context
if (!payload) { if (!payload) {
console.error('Payload instance not available. These hooks should be called within Payload server context or pass payload as parameter.') console.error('Payload instance not available. These hooks should be called within Payload server context or pass payload as parameter.')
return {} return {}
} }
const collectionSlug = getCollectionSlug(payload) const collectionSlug = getCollectionSlug(payload)
const result = await payload.find({ const result = await payload.find({
collection: collectionSlug, collection: collectionSlug,
where: { where: {
@@ -104,7 +105,7 @@ export async function getAllFeatureFlags(payload?: Payload): Promise<Record<stri
}) })
const flags: Record<string, FeatureFlag> = {} const flags: Record<string, FeatureFlag> = {}
for (const doc of result.docs) { for (const doc of result.docs) {
flags[doc.name as string] = { flags[doc.name as string] = {
name: doc.name as string, name: doc.name as string,
@@ -114,85 +115,85 @@ export async function getAllFeatureFlags(payload?: Payload): Promise<Record<stri
metadata: doc.metadata, metadata: doc.metadata,
} }
} }
return flags return flags
} catch (error) { } catch (error) {
console.error('Failed to fetch feature flags:', error) console.error('Failed to fetch feature flags:', error)
return {} return {}
} }
} })
/** /**
* Check if a user is in a feature rollout (for use in React Server Components) * Check if a user is in a feature rollout (for use in React Server Components)
*/ */
export async function isUserInRollout( export const isUserInRollout = cache(async (
flagName: string, flagName: string,
userId: string, userId: string,
payload?: Payload payload?: Payload
): Promise<boolean> { ): Promise<boolean> => {
const flag = await getFeatureFlag(flagName, payload) const flag = await getFeatureFlag(flagName, payload)
if (!flag?.enabled) { if (!flag?.enabled) {
return false return false
} }
if (!flag.rolloutPercentage || flag.rolloutPercentage === 100) { if (!flag.rolloutPercentage || flag.rolloutPercentage === 100) {
return true return true
} }
// Simple hash function for consistent user bucketing // Simple hash function for consistent user bucketing
const hash = userId.split('').reduce((acc, char) => { const hash = userId.split('').reduce((acc, char) => {
return ((acc << 5) - acc) + char.charCodeAt(0) return ((acc << 5) - acc) + char.charCodeAt(0)
}, 0) }, 0)
return (Math.abs(hash) % 100) < flag.rolloutPercentage return (Math.abs(hash) % 100) < flag.rolloutPercentage
} })
/** /**
* Get the variant for a user in an A/B test (for use in React Server Components) * Get the variant for a user in an A/B test (for use in React Server Components)
*/ */
export async function getUserVariant( export const getUserVariant = cache(async (
flagName: string, flagName: string,
userId: string, userId: string,
payload?: Payload payload?: Payload
): Promise<string | null> { ): Promise<string | null> => {
const flag = await getFeatureFlag(flagName, payload) const flag = await getFeatureFlag(flagName, payload)
if (!flag?.enabled || !flag.variants || flag.variants.length === 0) { if (!flag?.enabled || !flag.variants || flag.variants.length === 0) {
return null return null
} }
// Hash the user ID for consistent variant assignment // Hash the user ID for consistent variant assignment
const hash = Math.abs(userId.split('').reduce((acc, char) => { const hash = Math.abs(userId.split('').reduce((acc, char) => {
return ((acc << 5) - acc) + char.charCodeAt(0) return ((acc << 5) - acc) + char.charCodeAt(0)
}, 0)) }, 0))
const bucket = hash % 100 const bucket = hash % 100
let cumulative = 0 let cumulative = 0
for (const variant of flag.variants) { for (const variant of flag.variants) {
cumulative += variant.weight cumulative += variant.weight
if (bucket < cumulative) { if (bucket < cumulative) {
return variant.name return variant.name
} }
} }
return flag.variants[0]?.name || null return flag.variants[0]?.name || null
} })
/** /**
* Get feature flags by tags (for use in React Server Components) * Get feature flags by tags (for use in React Server Components)
*/ */
export async function getFeatureFlagsByTag(tag: string, payload?: Payload): Promise<FeatureFlag[]> { export const getFeatureFlagsByTag = cache(async (tag: string, payload?: Payload): Promise<FeatureFlag[]> => {
try { try {
// If no payload provided, return empty array as these hooks should be used within Payload context // If no payload provided, return empty array as these hooks should be used within Payload context
if (!payload) { if (!payload) {
console.error('Payload instance not available. These hooks should be called within Payload server context or pass payload as parameter.') console.error('Payload instance not available. These hooks should be called within Payload server context or pass payload as parameter.')
return [] return []
} }
const collectionSlug = getCollectionSlug(payload) const collectionSlug = getCollectionSlug(payload)
const result = await payload.find({ const result = await payload.find({
collection: collectionSlug, collection: collectionSlug,
where: { where: {
@@ -214,4 +215,4 @@ export async function getFeatureFlagsByTag(tag: string, payload?: Payload): Prom
console.error(`Failed to fetch feature flags with tag ${tag}:`, error) console.error(`Failed to fetch feature flags with tag ${tag}:`, error)
return [] return []
} }
} })