npm package: @joemark0008/feature-flags
Description: Framework-agnostic feature flags library with REST API integration
Author: JJ Gimeno
Latest version: 1.2.1

# @joemark0008/feature-flags

A framework-agnostic TypeScript library for feature flags with REST API integration and React support.

## Features

- 🎯 **Framework Agnostic**: Core library works with any JavaScript framework
- ⚛️ **React Integration**: Built-in hooks and provider for React applications
- 🌐 **REST API Support**: Flexible adapter system for any REST API backend
- 💾 **Multiple Storage Options**: REST API, localStorage, or in-memory storage
- 🔄 **Caching**: Intelligent caching with configurable timeout
- 🛡️ **Type Safe**: Full TypeScript support with strong typing
- 🎚️ **Fallback Strategy**: Graceful degradation when API is unavailable
- 📊 **Multi-tenant**: Organization and user-scoped flag management

## Installation

```bash
npm install @joemark0008/feature-flags
```

For React integration, ensure you have React as a peer dependency:

```bash
npm install react
```

### 🚀 Quick Start with Demo

Want to see it in action first? Try our complete demo:

```bash
git clone https://github.com/joemark0008/feature-flag.git
cd feature-flag/demo/feature-flags-demo
npm install
npm run dev
```

Open `http://localhost:5173` to explore all features interactively!

## Quick Start

### Basic Setup

```typescript
import { FeatureFlagManager, RestApiAdapter, createRestApiConfig } from '@joemark0008/feature-flags'

// Define your feature flags
const flagConfigs = [
  {
    key: 'dashboardAccess',
    label: 'Dashboard Access',
    description: 'Allow users to access the dashboard',
    category: 'access',
    defaultValue: false,
  },
  {
    key: 'newFeature',
    label: 'New Feature',
    description: 'Enable the new experimental feature',
    category: 'feature',
    defaultValue: false,
  },
]

// Configure REST API
const apiConfig = createRestApiConfig('https://your-api.com', 'your-auth-token')

// Create storage adapter
const storage = new RestApiAdapter(apiConfig)

// Initialize manager
const flagManager = new FeatureFlagManager(storage, flagConfigs, {
  cacheTimeout: 5 * 60 * 1000, // 5 minutes
  enableLocalStorage: true, // Fallback to localStorage
})

// Use in your application
const context = { organizationId: 'org-123', userId: 'user-456' }

const isDashboardEnabled = await flagManager.isEnabled('dashboardAccess', context)
if (isDashboardEnabled) {
  // Show dashboard
}
```

### React Integration

```tsx
import React from 'react'
import { FeatureFlagProvider, useFeatureFlag } from '@joemark0008/feature-flags/react'

function App() {
  const context = {
    organizationId: user.organizationId,
    userId: user.id,
  }

  return (
    <FeatureFlagProvider 
      manager={flagManager} 
      context={context}
      loadingComponent={LoadingSpinner}
    >
      <Dashboard />
    </FeatureFlagProvider>
  )
}

function Dashboard() {
  const hasDashboardAccess = useFeatureFlag('dashboardAccess')
  const hasNewFeature = useFeatureFlag('newFeature')

  if (!hasDashboardAccess) {
    return <AccessDenied />
  }

  return (
    <div>
      <h1>Dashboard</h1>
      {hasNewFeature && <NewFeatureComponent />}
    </div>
  )
}
```

## API Reference

### FeatureFlagManager

The core class for managing feature flags.

```typescript
class FeatureFlagManager {
  constructor(
    storage: FeatureFlagStorage,
    flagConfigs: FeatureFlagConfig[],
    options?: FeatureFlagManagerOptions
  )

  async isEnabled(flagKey: string, context: FeatureFlagContext): Promise<boolean>
  async getFlags(context: FeatureFlagContext): Promise<Record<string, boolean>>
  async setFlag(flagKey: string, enabled: boolean, context: FeatureFlagContext, scope?: 'organization' | 'user'): Promise<void>
  async setFlags(flags: Record<string, boolean>, context: FeatureFlagContext, scope?: 'organization' | 'user'): Promise<void>
  async getFlagDefinitions(): Promise<FeatureFlagConfig[]>
  clearCache(): void
}
```

### Storage Adapters

#### RestApiAdapter

```typescript
import { RestApiAdapter } from '@joemark0008/feature-flags'

const adapter = createFeatureFlagAdapter(process.env.NODE_ENV)
```

## Demo Application

🎯 **Try the live demo!** A complete React application showcasing all features:

```bash
cd demo/feature-flags-demo
npm install
npm run dev
```

**Demo Features:**
- 🔄 **Real-time flag toggling** with admin panel
- 🎨 **Dynamic theming** (light/dark mode)
- 💾 **localStorage persistence** 
- 📱 **Responsive design** with beautiful UI
- 🔧 **Developer tools** for debugging
- 📚 **Complete code examples**

The demo includes 5 sample feature flags demonstrating:
- Dashboard redesign toggle
- Dark mode theme switching  
- Premium features access control
- Beta chat functionality
- Advanced analytics display

[View Demo Source →](./demo/feature-flags-demo/)

## Examples
```

#### LocalStorageAdapter

```typescript
import { LocalStorageAdapter } from '@joemark0008/feature-flags'

const adapter = new LocalStorageAdapter('my-feature-flags')
```

#### MemoryAdapter

### Memory Adapter

For testing:

```typescript
import { MemoryAdapter } from '@joemark0008/feature-flags'

const memoryAdapter = new MemoryAdapter({ newDashboard: true }, flagConfigs)
const manager = new FeatureFlagManager(memoryAdapter, flagConfigs)
```

### GraphQL Adapter

For GraphQL APIs:

```typescript
import { GraphQLAdapter } from '@joemark0008/feature-flags'

const graphqlAdapter = new GraphQLAdapter({
  endpoint: 'https://your-api.com/graphql',
  headers: { Authorization: 'Bearer token' },
  authInterceptor: (headers) => ({
    ...headers,
    'X-API-Key': process.env.API_KEY
  })
})

const manager = new FeatureFlagManager(graphqlAdapter, flagConfigs)
```

#### Using GraphQL Adapter with Existing APIs

The GraphQL adapter is designed to work seamlessly with your existing GraphQL infrastructure. Here are common integration patterns:

**1. Custom Queries for Existing Schema**

Map the adapter to your existing GraphQL types:

```typescript
const adapter = new GraphQLAdapter({
  endpoint: 'https://your-existing-api.com/graphql',
  headers: {
    'Authorization': 'Bearer your-token'
  },
  queries: {
    // Map to your existing query structure
    getFlags: `
      query GetFeatureToggles($userId: ID!, $orgId: ID!) {
        user(id: $userId) {
          organization(id: $orgId) {
            featureToggles {
              name
              isActive
              metadata
            }
          }
        }
      }
    `,
    setFlag: `
      mutation UpdateToggle($input: UpdateToggleInput!) {
        updateFeatureToggle(input: $input) {
          id
          success
          toggle {
            name
            isActive
          }
        }
      }
    `
  }
})
```

**2. Integration with Apollo Client**

Reuse your existing Apollo Client setup:

```typescript
import { ApolloClient } from '@apollo/client'

const apolloClient = new ApolloClient({...})

// Use the same endpoint and auth
const featureFlagAdapter = new GraphQLAdapter({
  endpoint: apolloClient.link.options.uri,
  authInterceptor: (headers) => ({
    ...headers,
    // Reuse Apollo's auth logic
    'Authorization': apolloClient.cache.data?.authToken
  }),
  queries: {
    getFlags: `
      query GetFeatureFlags($context: FeatureFlagContextInput!) {
        currentUser {
          featureAccess(context: $context) {
            feature
            hasAccess
          }
        }
      }
    `
  }
})
```

**3. Relay Integration**

For Relay-style pagination and connections:

```typescript
// Use the built-in Relay support
const relayAdapter = GraphQLAdapter.withRelay({
  endpoint: 'https://your-relay-api.com/graphql',
  authInterceptor: (headers) => ({
    ...headers,
    'Authorization': `Bearer ${getAuthToken()}`
  })
})

// Or customize Relay queries
const customRelayAdapter = new GraphQLAdapter({
  endpoint: 'https://api.example.com/graphql',
  queries: {
    getFlags: `
      query GetFeatureFlags($context: FeatureFlagContextInput!, $first: Int, $after: String) {
        viewer {
          featureFlags(context: $context, first: $first, after: $after) {
            edges {
              node {
                key
                enabled
                rolloutPercentage
              }
            }
            pageInfo {
              hasNextPage
              endCursor
            }
          }
        }
      }
    `
  }
})
```

**4. Custom Data Transformation**

Handle different response formats by extending the adapter:

```typescript
class CustomGraphQLAdapter extends GraphQLAdapter {
  async getFlags(context: FeatureFlagContext): Promise<Record<string, boolean>> {
    const operation = {
      query: this.queries.getFlags,
      variables: { context },
    }
    
    const response = await this.executeGraphQL(operation)
    
    // Transform your custom response format
    const flags: Record<string, boolean> = {}
    
    // Example: nested structure transformation
    response.data?.user?.organization?.featureToggles?.forEach(toggle => {
      flags[toggle.name] = toggle.isActive
    })
    
    return flags
  }
}

const adapter = new CustomGraphQLAdapter({
  endpoint: 'https://your-api.com/graphql',
  // ... your config
})
```

**5. Environment-Specific Configuration**

Handle different environments with existing GraphQL endpoints:

```typescript
const createFeatureFlagAdapter = (environment: string) => {
  const configs = {
    development: {
      endpoint: 'http://localhost:4000/graphql',
      authInterceptor: (headers) => ({ 
        ...headers, 
        'x-api-key': 'dev-key' 
      })
    },
    production: {
      endpoint: 'https://api.yourcompany.com/graphql',
      authInterceptor: (headers) => ({ 
        ...headers, 
        'Authorization': `Bearer ${getProductionToken()}` 
      })
    }
  }

  return new GraphQLAdapter({
    ...configs[environment],
    queries: {
      getFlags: `
        query GetFeatureFlags($env: Environment!, $context: UserContext!) {
          featureFlags(environment: $env, context: $context) {
            key
            enabled
            rolloutPercentage
          }
        }
      `
    }
  })
}

const adapter = createFeatureFlagAdapter(process.env.NODE_ENV)
```

**6. Error Handling for Existing APIs**

Handle your API's specific error patterns:

```typescript
const adapter = new GraphQLAdapter({
  endpoint: 'https://your-api.com/graphql',
  authInterceptor: (headers) => {
    const token = localStorage.getItem('authToken')
    return {
      ...headers,
      'Authorization': token ? `Bearer ${token}` : undefined
    }
  },
  queries: {
    getFlags: `
      query GetFlags($context: FeatureFlagContextInput!) {
        featureAccess(input: $context) {
          ... on FeatureAccessSuccess {
            flags {
              name
              enabled
            }
          }
          ... on FeatureAccessError {
            message
            code
          }
        }
      }
    `
  }
})
```

### React Hooks

```typescript
// Get feature flags context
const { flags, loading, error, refetch } = useFeatureFlags()

// Check individual flag
const isEnabled = useFeatureFlag('flagKey')

// Toggle flags (admin UI)
const toggleFlag = useFeatureFlagToggle()
await toggleFlag('flagKey', true, 'organization')

// Get flags with state
const { flags, loading, error, refetch } = useFeatureFlagsWithState()
```

## Backend Integration

Your REST API should implement these endpoints:

```bash
# Get flags for context
GET /api/feature-flags?organizationId=org123&userId=user456
Response: { "dashboardAccess": true, "newFeature": false }

# Set individual flag
PUT /api/feature-flags/dashboardAccess
Body: { "enabled": true, "context": { "organizationId": "org123" }, "scope": "organization" }

PUT /api/feature-flags/:flagKey
Body: { "enabled": true, "context": { "organizationId": "org123" }, "scope": "organization" }

# Bulk update flags
PUT /api/feature-flags
Body: { 
  "flags": { "newDashboard": true, "betaFeatures": false },
  "context": { "organizationId": "org123" },
  "scope": "organization"
}

# Get flag definitions
GET /api/feature-flags/definitions
Response: [{ "key": "newDashboard", "label": "New Dashboard", ... }]
```

### GraphQL API Integration

For GraphQL APIs, your schema should support these operations:

```graphql
# Schema types
type FeatureFlag {
  key: String!
  enabled: Boolean!
}

type FeatureFlagDefinition {
  key: String!
  label: String!
  description: String!
  category: String!
  defaultValue: Boolean!
  dependencies: [String!]
}

input FeatureFlagContextInput {
  organizationId: String
  userId: String
  environment: String
}

input SetFeatureFlagInput {
  flagKey: String!
  enabled: Boolean!
  context: FeatureFlagContextInput!
  scope: String
}

# Queries
type Query {
  featureFlags(context: FeatureFlagContextInput!): [FeatureFlag!]!
  featureFlagDefinitions: [FeatureFlagDefinition!]!
}

# Mutations
type Mutation {
  setFeatureFlag(input: SetFeatureFlagInput!): SetFeatureFlagResult!
  setFeatureFlags(input: SetFeatureFlagsInput!): SetFeatureFlagsResult!
}
```

## Configuration

### Manager Options

```typescript
interface FeatureFlagManagerOptions {
  cacheTimeout?: number // Cache timeout in ms (default: 5 minutes)
  fallbackToDefaults?: boolean // Use defaults on error (default: true)
  enableLocalStorage?: boolean // Use localStorage fallback (default: true)
  onError?: (error: Error) => void // Error handler
}
```

### Flag Configuration

```typescript
interface FeatureFlagConfig {
  key: string // Unique identifier
  label: string // Human-readable name
  description: string // Description for admin UI
  category: 'access' | 'feature' | 'experiment' // Flag category
  defaultValue: boolean // Default value when not set
  dependencies?: string[] // Other flags this depends on
}
```

### Context

```typescript
interface FeatureFlagContext {
  organizationId?: string // Organization scope
  userId?: string // User scope
  userRoles?: string[] // User roles for permission-based flags
  environment?: string // Environment (dev, staging, prod)
  [key: string]: any // Additional context properties
}
```

## Advanced Usage

### Custom Authentication

```typescript
// REST API with custom auth
const apiAdapter = new RestApiAdapter({
  baseUrl: 'https://your-api.com',
  endpoints: { /* ... */ },
  authInterceptor: (config) => ({
    ...config,
    headers: {
      ...config.headers,
      'Authorization': `Bearer ${getAuthToken()}`,
      'X-API-Key': process.env.API_KEY
    }
  })
})

// GraphQL with custom authentication
const graphqlAdapter = new GraphQLAdapter({
  endpoint: 'https://your-api.com/graphql',
  authInterceptor: (headers) => ({
    ...headers,
    'Authorization': `Bearer ${getAuthToken()}`,
    'X-Tenant-ID': getCurrentTenant()
  })
})
```

### Custom GraphQL Queries

```typescript
const customGraphQLAdapter = GraphQLAdapter.withCustomQueries({
  endpoint: 'https://your-api.com/graphql',
  queries: {
    getFlags: `
      query GetUserFeatureFlags($userId: ID!, $orgId: ID!) {
        user(id: $userId) {
          organization(id: $orgId) {
            featureFlags {
              name
              isEnabled
              rolloutPercentage
            }
          }
        }
      }
    `,
    setFlag: `
      mutation ToggleFeature($input: ToggleFeatureInput!) {
        toggleFeature(input: $input) {
          feature {
            name
            enabled
          }
          errors {
            field
            message
          }
        }
      }
    `
  }
})

// Relay-style pagination support
const relayAdapter = GraphQLAdapter.withRelay({
  endpoint: 'https://your-api.com/graphql',
  headers: { Authorization: 'Bearer token' }
})
```

## Examples

### Admin Toggle Component

```tsx
import { useFeatureFlagToggle, useFeatureFlags } from '@joemark0008/feature-flags/react'

function AdminPanel() {
  const { flags } = useFeatureFlags()
  const toggleFlag = useFeatureFlagToggle()

  const handleToggle = async (flagKey: string) => {
    try {
      await toggleFlag(flagKey, !flags[flagKey])
    } catch (error) {
      console.error('Failed to toggle flag:', error)
    }
  }

  return (
    <div>
      {Object.entries(flags).map(([key, enabled]) => (
        <div key={key}>
          <label>
            <input
              type="checkbox"
              checked={enabled}
              onChange={() => handleToggle(key)}
            />
            {key}
          </label>
        </div>
      ))}
    </div>
  )
}
```

### Custom Storage Adapter

```typescript
import { FeatureFlagStorage } from '@joemark0008/feature-flags'

class GraphQLAdapter implements FeatureFlagStorage {
  async getFlags(context: FeatureFlagContext): Promise<Record<string, boolean>> {
    // Implement GraphQL query
  }

  async setFlag(flagKey: string, enabled: boolean, context: FeatureFlagContext): Promise<void> {
    // Implement GraphQL mutation
  }

  // ... other methods
}
```

## License

MIT

## Contributing

We welcome contributions! Please see our contributing guidelines for more details.