-
Notifications
You must be signed in to change notification settings - Fork 60
Adds devunus package #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chrizy
wants to merge
5
commits into
vercel:main
Choose a base branch
from
devunus:adapter-devunus
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Adds devunus package #105
Changes from 3 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| module.exports = { | ||
| extends: ['custom'], | ||
| root: true, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { | ||
| describe, | ||
| it, | ||
| expect, | ||
| vi, | ||
| beforeAll, | ||
| afterAll, | ||
| afterEach, | ||
| beforeEach, | ||
| } from 'vitest'; | ||
| import { devunusAdapter } 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'); | ||
| return HttpResponse.json({ | ||
| flags: [ | ||
| { | ||
| id: 'flag1', | ||
| name: 'enableFeatureX', | ||
| description: 'Enable feature X', | ||
| value: true, | ||
| type: 'boolean', | ||
| createdAt: 1615000000000, | ||
| updatedAt: 1620000000000, | ||
| }, | ||
| ], | ||
| baseUrl: 'https://app.devunus.com/admin/333/project/1', | ||
| }); | ||
| }), | ||
| ); | ||
|
|
||
| beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); | ||
| afterAll(() => server.close()); | ||
| afterEach(() => server.resetHandlers()); | ||
|
|
||
| describe('Devunus default adapter', () => { | ||
| const originalEnv = process.env; | ||
|
|
||
| beforeEach(() => { | ||
| vi.resetModules(); | ||
| process.env = { ...originalEnv }; | ||
| process.env.DEVUNUS_ENV_KEY = 'test-key'; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| process.env = originalEnv; | ||
| }); | ||
|
|
||
| it('should create an adapter with correct interface', () => { | ||
| const adapter = devunusAdapter.getFeature(); | ||
| expect(adapter.provider).toBe('devunus'); | ||
| expect(typeof adapter.getProviderData).toBe('function'); | ||
| }); | ||
|
|
||
| it('should pass environment variables to provider', async () => { | ||
| const customKey = 'custom-key'; | ||
| process.env.DEVUNUS_ENV_KEY = customKey; | ||
| const adapter = devunusAdapter.getFeature(); | ||
| await adapter.getProviderData(); | ||
| // We only test that the adapter properly passes the env var | ||
| // The actual API response handling is tested in provider tests | ||
| }); | ||
|
|
||
| it('should construct correct origin URL with project ID', async () => { | ||
| process.env.DEVUNUS_PROJECT_ID = 'test-project'; | ||
| const adapter = devunusAdapter.getFeature(); | ||
| const result = await adapter.getProviderData(); | ||
|
|
||
| const featureX = result.definitions.enableFeatureX; | ||
| expect(featureX?.origin).toEqual( | ||
| 'https://app.devunus.com/admin/333/project/1/flag/flag1', | ||
| ); | ||
| }); | ||
|
|
||
| it('should properly initialize with default configuration', () => { | ||
| const adapter = devunusAdapter.getFeature(); | ||
| expect(adapter).toBeDefined(); | ||
| expect(adapter.provider).toBe('devunus'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { getProviderData } from './provider'; | ||
| export * from './provider'; | ||
|
|
||
| /** | ||
| * Default adapter that uses environment variables for configuration | ||
| */ | ||
| export const devunusAdapter = { | ||
| getFeature: () => ({ | ||
| provider: 'devunus', | ||
| getProviderData: () => | ||
| getProviderData({ | ||
| envKey: process.env.DEVUNUS_ENV_KEY || '', | ||
| }), | ||
| }), | ||
| }; | ||
|
|
||
| export default devunusAdapter; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| }`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"] | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.