From 42bdb832d0465f3f1d0750a595eca95ae3525615 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sun, 28 Sep 2025 22:12:57 +0200 Subject: [PATCH] Bump version to 0.0.6 --- package.json | 2 +- src/hooks/server.ts | 73 +++++++++++++++++++++++---------------------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index a27d608..8e1e21c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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", "license": "MIT", "type": "module", diff --git a/src/hooks/server.ts b/src/hooks/server.ts index 9044147..ca6a3a4 100644 --- a/src/hooks/server.ts +++ b/src/hooks/server.ts @@ -1,4 +1,5 @@ import { Payload } from 'payload' +import { cache } from "react" export interface FeatureFlag { name: string @@ -16,10 +17,10 @@ export interface FeatureFlag { function getCollectionSlug(payload: Payload): string { try { // Look for the feature flags collection - it should have a 'name' field with unique constraint - const collection = payload.config.collections?.find(col => - col.fields.some((field: any) => - field.name === 'name' && - field.type === 'text' && + const collection = payload.config.collections?.find(col => + col.fields.some((field: any) => + field.name === 'name' && + field.type === 'text' && field.unique === true ) && 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) */ -export async function getFeatureFlag(flagName: string, payload?: Payload): Promise { +export const getFeatureFlag = cache(async (flagName: string, payload?: Payload): Promise => { try { // If no payload provided, return null as these hooks should be used within Payload context if (!payload) { console.error('Payload instance not available. These hooks should be called within Payload server context or pass payload as parameter.') return null } - + const collectionSlug = getCollectionSlug(payload) - + const result = await payload.find({ collection: collectionSlug, where: { @@ -58,7 +59,7 @@ export async function getFeatureFlag(flagName: string, payload?: Payload): Promi } const flag = result.docs[0] - + return { name: flag.name as string, 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) return null } -} +}) /** * Check if a feature flag is enabled (for use in React Server Components) */ -export async function isFeatureEnabled(flagName: string, payload?: Payload): Promise { +export const isFeatureEnabled = cache(async (flagName: string, payload?: Payload): Promise => { const flag = await getFeatureFlag(flagName, payload) return flag?.enabled ?? false -} +}) /** * Get all active feature flags (for use in React Server Components) */ -export async function getAllFeatureFlags(payload?: Payload): Promise> { +export const getAllFeatureFlags = cache(async (payload?: Payload): Promise> => { try { // If no payload provided, return empty object as these hooks should be used within Payload context if (!payload) { console.error('Payload instance not available. These hooks should be called within Payload server context or pass payload as parameter.') return {} } - + const collectionSlug = getCollectionSlug(payload) - + const result = await payload.find({ collection: collectionSlug, where: { @@ -104,7 +105,7 @@ export async function getAllFeatureFlags(payload?: Payload): Promise = {} - + for (const doc of result.docs) { flags[doc.name as string] = { name: doc.name as string, @@ -114,85 +115,85 @@ export async function getAllFeatureFlags(payload?: Payload): Promise { +): Promise => { const flag = await getFeatureFlag(flagName, payload) - + if (!flag?.enabled) { return false } - + if (!flag.rolloutPercentage || flag.rolloutPercentage === 100) { return true } - + // Simple hash function for consistent user bucketing const hash = userId.split('').reduce((acc, char) => { return ((acc << 5) - acc) + char.charCodeAt(0) }, 0) - + return (Math.abs(hash) % 100) < flag.rolloutPercentage -} +}) /** * 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, userId: string, payload?: Payload -): Promise { +): Promise => { const flag = await getFeatureFlag(flagName, payload) - + if (!flag?.enabled || !flag.variants || flag.variants.length === 0) { return null } - + // Hash the user ID for consistent variant assignment const hash = Math.abs(userId.split('').reduce((acc, char) => { return ((acc << 5) - acc) + char.charCodeAt(0) }, 0)) - + const bucket = hash % 100 let cumulative = 0 - + for (const variant of flag.variants) { cumulative += variant.weight if (bucket < cumulative) { return variant.name } } - + return flag.variants[0]?.name || null -} +}) /** * Get feature flags by tags (for use in React Server Components) */ -export async function getFeatureFlagsByTag(tag: string, payload?: Payload): Promise { +export const getFeatureFlagsByTag = cache(async (tag: string, payload?: Payload): Promise => { try { // If no payload provided, return empty array as these hooks should be used within Payload context if (!payload) { console.error('Payload instance not available. These hooks should be called within Payload server context or pass payload as parameter.') return [] } - + const collectionSlug = getCollectionSlug(payload) - + const result = await payload.find({ collection: collectionSlug, 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) return [] } -} \ No newline at end of file +})