Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/adapter-devunus/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
extends: ['custom'],
root: true,
};
7 changes: 7 additions & 0 deletions packages/adapter-devunus/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# @flags-sdk/devunus

## 0.1.0

### Initial Release

- Initial release of the Devunus adapter for flags-sdk
35 changes: 35 additions & 0 deletions packages/adapter-devunus/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# @flags-sdk/devunus

Devunus adapter for [flags-sdk](https://github.com/vercel/flags).

- An adapter for loading feature flags from devunus (coming soon).
- A getProviderData function for use with the Flags Explorer (available today).

## Installation

```bash
npm install @flags-sdk/devunus
```

## Usage getProviderData

Use a server env key for DEVUNUS_ENV_KEY. You can find your environment key in the [Devunus Admin Console](https://app.devunus.com/admin/def/project/1/get-started/e0-0/keys).

`app/.well-known/vercel/flags/route.ts`:

```tsx
import { verifyAccess, type ApiData } from 'flags';
import { getProviderData } from '@flags-sdk/devunus';
import { NextResponse, type NextRequest } from 'next/server';

export async function GET(request: NextRequest) {
const access = await verifyAccess(request.headers.get('Authorization'));
if (!access) return NextResponse.json(null, { status: 401 });

const flagData = await getProviderData({
envKey: process.env.DEVUNUS_ENV_KEY,
});

return NextResponse.json<ApiData>(flagData);
}
```
57 changes: 57 additions & 0 deletions packages/adapter-devunus/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@flags-sdk/devunus",
"version": "0.1.0",
"description": "Devunus adapter for flags-sdk",
"keywords": [
"feature-flags",
"devunus",
"flags-sdk"
],
"license": "MIT",
"author": "",
"sideEffects": false,
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"main": "./dist/index.js",
"typesVersions": {
"*": {
".": [
"dist/*.d.ts",
"dist/*.d.cts"
]
}
},
"files": [
"dist",
"CHANGELOG.md"
],
"scripts": {
"build": "rimraf dist && tsup",
"dev": "tsup --watch --clean=false",
"eslint": "eslint-runner",
"eslint:fix": "eslint-runner --fix",
"test": "vitest --run",
"test:watch": "vitest",
"type-check": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "20.11.17",
"eslint-config-custom": "workspace:*",
"eslint-plugin-vitest": "0.5.4",
"flags": "workspace:*",
"msw": "2.6.4",
"rimraf": "6.0.1",
"tsconfig": "workspace:*",
"tsup": "8.0.1",
"typescript": "5.6.3",
"vitest": "1.4.0"
},
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions packages/adapter-devunus/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getProviderData } from './provider';
74 changes: 74 additions & 0 deletions packages/adapter-devunus/src/provider/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { getProviderData } from './index';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

const server = setupServer(
http.get('https://api.devunus.com/api/flags', ({ request }) => {
const authHeader = request.headers.get('Authorization');

if (!authHeader || authHeader !== 'valid-key') {
return new HttpResponse(null, { status: 401 });
}

return HttpResponse.json({
flags: [
{
id: 'flag1',
name: 'enableFeatureX',
description: 'Enable feature X',
value: true,
type: 'boolean',
createdAt: 1615000000000,
updatedAt: 1620000000000,
},
{
id: 'flag2',
name: 'userTheme',
description: 'User theme preference',
value: 'dark',
type: 'string',
createdAt: 1615000000000,
updatedAt: 1620000000000,
},
],
baseUrl: 'https://app.devunus.com/admin/333/project/1',
});
}),
);

describe('Devunus provider', () => {
beforeAll(() => server.listen());
afterAll(() => server.close());

it('should return empty definitions and a hint when no env key is provided', async () => {
const result = await getProviderData({ envKey: '' });

expect(result.definitions).toEqual({});
expect(result.hints).toBeDefined();
expect(result.hints?.length).toBe(1);
expect(result.hints?.[0]?.key).toBe('devunus/missing-env-key');
});

it('should return empty definitions and a hint when the API returns an error', async () => {
const result = await getProviderData({ envKey: 'invalid-key' });

expect(result.definitions).toEqual({});
expect(result.hints).toBeDefined();
expect(result.hints?.length).toBe(1);
expect(result.hints?.[0]?.key).toBe('devunus/response-not-ok');
});

it('should return flag definitions when the API returns valid data', async () => {
const result = await getProviderData({ envKey: 'valid-key' });

expect(Object.keys(result.definitions)).toHaveLength(2);
expect(result.definitions.enableFeatureX).toBeDefined();
expect(result.definitions.userTheme).toBeDefined();
const enableFeatureX = result.definitions.enableFeatureX;
const userTheme = result.definitions.userTheme;
expect(enableFeatureX?.description).toBe('Enable feature X');
expect(userTheme?.description).toBe('User theme preference');
expect(result.hints?.length).toBe(0);
});
});
98 changes: 98 additions & 0 deletions packages/adapter-devunus/src/provider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { ProviderData, JsonValue } from 'flags';

interface DevunusFlag {
id: string;
name: string;
description: string;
value: string;
type: 'boolean' | 'string' | 'number' | 'json';
createdAt: number;
updatedAt: number;
}

interface DevunusResponse {
flags: DevunusFlag[];
baseUrl: string;
}

export async function getProviderData(options: {
/**
* The Devunus environment key.
*/
envKey: string;
}): Promise<ProviderData> {
if (!options.envKey) {
return {
definitions: {},
hints: [
{
key: 'devunus/missing-env-key',
text: 'Missing DevUnus environment key',
},
],
};
}

try {
// Get from edge API
const response = await fetch(`https://api.devunus.com/api/flags`, {
headers: { Authorization: `${options.envKey}` },
cache: 'no-store',
});

if (response.status !== 200) {
return {
definitions: {},
hints: [
{
key: 'devunus/response-not-ok',
text: `Failed to fetch DevUnus flag definitions (received ${response.status} response)`,
},
],
};
}

const data = (await response.json()) as DevunusResponse;
const { flags, baseUrl } = data;

const definitions: ProviderData['definitions'] = {};

for (const flag of flags) {
const flagOptions = [];

// For boolean flags, provide true/false options
if (flag.type === 'boolean') {
flagOptions.push({ value: true, label: 'On' });
flagOptions.push({ value: false, label: 'Off' });
}

// For string flags, include the current value as an option
if (flag.type === 'string') {
flagOptions.push({ value: flag.value });
}

definitions[flag.name] = {
description: flag.description,
options: flagOptions,
origin: `${baseUrl}/flag/${flag.id}`,
updatedAt: flag.updatedAt,
createdAt: flag.createdAt,
defaultValue: flag.value,
};
}

return { definitions, hints: [] };
} catch (error) {
return {
definitions: {},
hints: [
{
key: 'devunus/unexpected-error',
text: `Unexpected error fetching DevUnus flag definitions: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
};
}
}
9 changes: 9 additions & 0 deletions packages/adapter-devunus/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "tsconfig/base.json",
"include": ["src"],
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "dist",
"types": ["node", "vitest/globals"]
}
}
12 changes: 12 additions & 0 deletions packages/adapter-devunus/tsup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from 'tsup';

export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
treeshake: true,
minify: true,
});
8 changes: 8 additions & 0 deletions packages/adapter-devunus/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
environment: 'node',
globals: true,
},
});
Loading