Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ jobs:

- name: Check PR description
run: |
PR_BODY="${{ github.event.pull_request.body }}"
# Use heredoc to safely capture PR body without shell interpretation
PR_BODY=$(cat << 'EOF'
${{ github.event.pull_request.body }}
EOF
)

if [ -z "$PR_BODY" ] || [ "$PR_BODY" == "null" ]; then
echo "⚠️ PR description is empty. Consider adding a description."
Expand All @@ -46,9 +50,13 @@ jobs:

- name: Check for linked issues
run: |
PR_BODY="${{ github.event.pull_request.body }}"
# Use heredoc to safely capture PR body without shell interpretation
PR_BODY=$(cat << 'EOF'
${{ github.event.pull_request.body }}
EOF
)

if echo "$PR_BODY" | grep -iE "(closes|fixes|resolves) #[0-9]+"; then
if echo "${PR_BODY}" | grep -iE "(closes|fixes|resolves) #[0-9]+"; then
echo "✅ PR links to an issue"
else
echo "ℹ️ No linked issues found. Consider linking related issues."
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ Togglekit gives you a minimal, modern alternative to heavy SaaS feature flag pla

Planned components:

- ✅ TypeScript core evaluator
- ✅ TypeScript core evaluator (M2)
- ✅ Node SDK (M3)
- ✅ Web SDK with React hooks (M3)
- ⬜ Rust core engine
- ⬜ Node N-API binding
- ⬜ WASM engine for web
Expand Down
19 changes: 19 additions & 0 deletions packages/flags-client-node/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }]
},
"ignorePatterns": ["dist", "node_modules", "test", "**/*.d.ts", "**/*.js"]
}

12 changes: 12 additions & 0 deletions packages/flags-client-node/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Compiled output
dist/
*.tsbuildinfo

# Generated files in src (from composite builds)
src/**/*.d.ts
src/**/*.d.ts.map
src/**/*.js
src/**/*.js.map
!src/**/*.test.ts
!src/**/*.test.js

231 changes: 231 additions & 0 deletions packages/flags-client-node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# @togglekit/flags-client-node

Node.js SDK for Togglekit feature flags.

## Installation

```bash
npm install @togglekit/flags-client-node
# or
pnpm add @togglekit/flags-client-node
# or
yarn add @togglekit/flags-client-node
```

## Quick Start

```typescript
import { createFlagClient } from '@togglekit/flags-client-node';

// Create client with API connection
const client = await createFlagClient({
apiUrl: 'https://api.togglekit.com',
apiKey: 'your-api-key',
refreshInterval: 30000, // Refresh every 30 seconds
});

// Or use static configuration (for testing/development)
const client = await createFlagClient({
staticConfig: {
'dark-mode': {
key: 'dark-mode',
defaultValue: false,
rules: [
{
conditions: [
{ attribute: 'plan', operator: 'eq', value: 'premium' }
],
value: true
}
]
}
}
});

// Evaluate a boolean flag
const result = client.getBool('dark-mode', {
userId: 'user-123',
attributes: { plan: 'premium' }
});

console.log(result.value); // true
console.log(result.reason); // 'rule_match'

// Evaluate a variant flag
const layout = client.getVariant('pricing-layout', {
userId: 'user-123',
attributes: { country: 'US' }
});

console.log(layout.value); // 'layout-a', 'layout-b', or 'control'

// Clean up when done
client.close();
```

## API Reference

### `createFlagClient(options)`

Creates a new flag client instance.

**Options:**

- `apiUrl` (string, optional): API URL for fetching flag configuration
- `apiKey` (string, optional): API key for authentication
- `staticConfig` (FlagConfig, optional): Static configuration (alternative to API)
- `refreshInterval` (number, optional): Interval in milliseconds for refreshing config (default: 30000)
- `enableRefresh` (boolean, optional): Enable automatic config refresh (default: true)
- `timeout` (number, optional): Timeout for API requests in milliseconds (default: 5000)

**Returns:** `Promise<FlagClient>`

### `FlagClient`

#### `getBool(flagKey, context): EvaluationResult<boolean>`

Evaluates a boolean feature flag.

**Parameters:**

- `flagKey` (string): The flag key to evaluate
- `context` (Context): User context with optional userId and attributes

**Returns:** Evaluation result with boolean value and reason

#### `getVariant(flagKey, context): EvaluationResult<string>`

Evaluates a variant feature flag.

**Parameters:**

- `flagKey` (string): The flag key to evaluate
- `context` (Context): User context with optional userId and attributes

**Returns:** Evaluation result with variant key (string) and reason

#### `refresh(): Promise<void>`

Manually refreshes the configuration from the API.

#### `close(): void`

Closes the client and stops any refresh timers.

#### `hasFlag(flagKey): boolean`

Checks if a flag exists in the configuration.

#### `getFlagKeys(): string[]`

Returns all flag keys in the configuration.

#### `getConfig(): FlagConfig`

Returns the current flag configuration.

#### `isReady(): boolean`

Checks if the client is initialized and ready.

## Context

The context object is used to evaluate flags based on user attributes:

```typescript
interface Context {
userId?: string; // For rollout bucketing
attributes: Record<string, unknown>; // Custom attributes
}
```

**Examples:**

```typescript
// Simple context
const result = client.getBool('feature', {
attributes: { plan: 'premium' }
});

// With userId for rollouts
const result = client.getBool('beta-feature', {
userId: 'user-123',
attributes: { country: 'US', plan: 'free' }
});
```

## Evaluation Reasons

The evaluation result includes a reason indicating why a particular value was returned:

- `'default'`: No rules matched, using default value
- `'rule_match'`: A rule matched and was applied
- `'rollout'`: User is included in percentage rollout
- `'rollout_excluded'`: User is excluded from percentage rollout
- `'flag_not_found'`: Flag key doesn't exist in configuration
- `'error'`: Error during evaluation

## Advanced Usage

### Custom Fetch Function

```typescript
import fetch from 'node-fetch';

const client = await createFlagClient({
apiUrl: 'https://api.togglekit.com',
apiKey: 'your-api-key',
fetchFn: fetch as any
});
```

### Disable Automatic Refresh

```typescript
const client = await createFlagClient({
apiUrl: 'https://api.togglekit.com',
apiKey: 'your-api-key',
enableRefresh: false
});

// Manually refresh when needed
await client.refresh();
```

### Error Handling

```typescript
try {
const client = await createFlagClient({
apiUrl: 'https://api.togglekit.com',
apiKey: 'invalid-key'
});
} catch (error) {
console.error('Failed to initialize client:', error);
}

// Evaluation never throws - returns safe defaults
const result = client.getBool('non-existent', { attributes: {} });
console.log(result.value); // false
console.log(result.reason); // 'flag_not_found'
```

## TypeScript

This package is written in TypeScript and includes full type definitions.

```typescript
import {
createFlagClient,
FlagClient,
FlagClientOptions,
Context,
EvaluationResult,
FlagConfig
} from '@togglekit/flags-client-node';
```

## License

MIT

23 changes: 23 additions & 0 deletions packages/flags-client-node/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
moduleNameMapper: {
'^@togglekit/flags-core-ts$': '<rootDir>/../flags-core-ts/src',
},
};

14 changes: 12 additions & 2 deletions packages/flags-client-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "echo 'Tests not yet implemented'",
"lint": "eslint src"
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src --ext .ts"
},
"dependencies": {
"@togglekit/flags-core-ts": "workspace:*"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"@types/node": "^20.11.0",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"eslint": "^8.56.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"typescript": "^5.3.3"
}
}


Loading
Loading