Refactor feature flag utilities to inject Payload instance, add strict types, and update .npmignore settings

This commit is contained in:
2025-09-12 15:57:41 +02:00
parent 48834c6fa2
commit 2fdef92d62
5 changed files with 85 additions and 28 deletions

50
.npmignore Normal file
View File

@@ -0,0 +1,50 @@
# Source files
src/
dev/
.next/
.turbo/
# Development files
*.log
.env*
.DS_Store
*.tsbuildinfo
.turbo
.cache
.temp
.tmp
# Test files
*.test.*
*.spec.*
e2e/
tests/
__tests__/
# Development dependencies
node_modules/
coverage/
# Build configs
.swcrc
tsconfig.json
vitest.config.ts
playwright.config.ts
next.config.mjs
eslint.config.js
# Documentation (keep README.md)
docs/
.github/
# IDE
.vscode/
.idea/
*.swp
*.swo
# Only include built files
!dist/
!package.json
!README.md
!LICENSE

View File

@@ -116,5 +116,6 @@
] ]
}, },
"registry": "https://registry.npmjs.org/", "registry": "https://registry.npmjs.org/",
"dependencies": {} "dependencies": {},
"packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184"
} }

View File

@@ -3,7 +3,7 @@ import type { PayloadHandler } from 'payload'
export const customEndpointHandler = (collectionSlug: string): PayloadHandler => export const customEndpointHandler = (collectionSlug: string): PayloadHandler =>
async (req) => { async (req) => {
const { payload } = req const { payload } = req
const url = new URL(req.url) const url = new URL(req.url || '')
const pathParts = url.pathname.split('/').filter(Boolean) const pathParts = url.pathname.split('/').filter(Boolean)
const flagName = pathParts[pathParts.length - 1] const flagName = pathParts[pathParts.length - 1]

View File

@@ -1,5 +1,4 @@
import { getPayload } from 'payload' import { getPayload, type Payload } from 'payload'
import configPromise from '@payload-config'
export interface FeatureFlag { export interface FeatureFlag {
name: string name: string
@@ -14,16 +13,15 @@ export interface FeatureFlag {
} }
// Helper to get the collection slug from config // Helper to get the collection slug from config
async function getCollectionSlug(): Promise<string> { function getCollectionSlug(payload: Payload): string {
const payload = await getPayload({ config: configPromise })
// 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 => 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 => field.name === 'enabled' && field.type === 'checkbox') col.fields.some((field: any) => field.name === 'enabled' && field.type === 'checkbox')
) )
return collection?.slug || 'feature-flags' return collection?.slug || 'feature-flags'
} }
@@ -31,10 +29,12 @@ async function getCollectionSlug(): Promise<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): Promise<FeatureFlag | null> { export async function getFeatureFlag(flagName: string, payload?: Payload): Promise<FeatureFlag | null> {
try { try {
const payload = await getPayload({ config: configPromise }) if (!payload) {
const collectionSlug = await getCollectionSlug() throw new Error('Payload instance is required')
}
const collectionSlug = getCollectionSlug(payload)
const result = await payload.find({ const result = await payload.find({
collection: collectionSlug, collection: collectionSlug,
@@ -68,18 +68,20 @@ export async function getFeatureFlag(flagName: string): Promise<FeatureFlag | nu
/** /**
* 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): Promise<boolean> { export async function isFeatureEnabled(flagName: string, payload?: Payload): Promise<boolean> {
const flag = await getFeatureFlag(flagName) 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(): Promise<Record<string, FeatureFlag>> { export async function getAllFeatureFlags(payload?: Payload): Promise<Record<string, FeatureFlag>> {
try { try {
const payload = await getPayload({ config: configPromise }) if (!payload) {
const collectionSlug = await getCollectionSlug() throw new Error('Payload instance is required')
}
const collectionSlug = getCollectionSlug(payload)
const result = await payload.find({ const result = await payload.find({
collection: collectionSlug, collection: collectionSlug,
@@ -115,9 +117,10 @@ export async function getAllFeatureFlags(): Promise<Record<string, FeatureFlag>>
*/ */
export async function isUserInRollout( export async function isUserInRollout(
flagName: string, flagName: string,
userId: string userId: string,
payload?: Payload
): Promise<boolean> { ): Promise<boolean> {
const flag = await getFeatureFlag(flagName) const flag = await getFeatureFlag(flagName, payload)
if (!flag?.enabled) { if (!flag?.enabled) {
return false return false
@@ -140,9 +143,10 @@ export async function isUserInRollout(
*/ */
export async function getUserVariant( export async function getUserVariant(
flagName: string, flagName: string,
userId: string userId: string,
payload?: Payload
): Promise<string | null> { ): Promise<string | null> {
const flag = await getFeatureFlag(flagName) 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
@@ -169,10 +173,12 @@ export async function getUserVariant(
/** /**
* 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): Promise<FeatureFlag[]> { export async function getFeatureFlagsByTag(tag: string, payload?: Payload): Promise<FeatureFlag[]> {
try { try {
const payload = await getPayload({ config: configPromise }) if (!payload) {
const collectionSlug = await getCollectionSlug() throw new Error('Payload instance is required')
}
const collectionSlug = getCollectionSlug(payload)
const result = await payload.find({ const result = await payload.find({
collection: collectionSlug, collection: collectionSlug,

View File

@@ -89,7 +89,7 @@ export const payloadFeatureFlags =
...(enableRollouts ? [ ...(enableRollouts ? [
{ {
name: 'rolloutPercentage', name: 'rolloutPercentage',
type: 'number', type: 'number' as const,
min: 0, min: 0,
max: 100, max: 100,
defaultValue: 100, defaultValue: 100,
@@ -102,7 +102,7 @@ export const payloadFeatureFlags =
...(enableVariants ? [ ...(enableVariants ? [
{ {
name: 'variants', name: 'variants',
type: 'array', type: 'array' as const,
admin: { admin: {
description: 'Define variants for A/B testing', description: 'Define variants for A/B testing',
condition: (data: any) => data?.enabled === true, condition: (data: any) => data?.enabled === true,
@@ -110,7 +110,7 @@ export const payloadFeatureFlags =
fields: [ fields: [
{ {
name: 'name', name: 'name',
type: 'text', type: 'text' as const,
required: true, required: true,
admin: { admin: {
description: 'Variant identifier (e.g., control, variant-a)', description: 'Variant identifier (e.g., control, variant-a)',
@@ -118,7 +118,7 @@ export const payloadFeatureFlags =
}, },
{ {
name: 'weight', name: 'weight',
type: 'number', type: 'number' as const,
min: 0, min: 0,
max: 100, max: 100,
required: true, required: true,
@@ -128,7 +128,7 @@ export const payloadFeatureFlags =
}, },
{ {
name: 'metadata', name: 'metadata',
type: 'json', type: 'json' as const,
admin: { admin: {
description: 'Additional data for this variant', description: 'Additional data for this variant',
}, },