Skip to content

Commit fb6432e

Browse files
authored
Add support for feature flags management API (#1420)
## Description - Adds the following methods to support the feature flags management API - `listFeatureFlags` (`GET /feature-flags`) - `getFeatureFlag` (`GET /feature-flags/:slug`) - `enableFeatureFlag` (`PUT /feature-flags/:slug/enable`) - `disableFeatureFlag` (`PUT /feature-flags/:slug/disable`) - `addFlagTarget` (`POST /feature-flags/:slug/targets/:targetId`) - `removeFlagTarget` (`DELETE /feature-flags/:slug/targets/:targetId`) - Updates `FeatureFlag` interface to add `tags`, `enabled` and `defaultValue` fields - Updates `FeatureFlag.description` to be a required field to match the API response ## Documentation Does this require changes to the WorkOS Docs? E.g. the [API Reference](https://workos.com/docs/reference) or code snippets need updates. ``` [x] Yes ``` If yes, link a related docs PR and add a docs maintainer as a reviewer. Their approval is required.
1 parent efe821c commit fb6432e

20 files changed

+447
-13
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import fetch from 'jest-fetch-mock';
2+
import {
3+
fetchOnce,
4+
fetchURL,
5+
fetchSearchParams,
6+
fetchMethod,
7+
} from '../common/utils/test-utils';
8+
import { WorkOS } from '../workos';
9+
import listFeatureFlagsFixture from './fixtures/list-feature-flags.json';
10+
import getFeatureFlagFixture from './fixtures/get-feature-flag.json';
11+
import enableFeatureFlagFixture from './fixtures/enable-feature-flag.json';
12+
import disableFeatureFlagFixture from './fixtures/disable-feature-flag.json';
13+
14+
const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');
15+
16+
describe('FeatureFlags', () => {
17+
beforeEach(() => fetch.resetMocks());
18+
19+
describe('listFeatureFlags', () => {
20+
describe('without any options', () => {
21+
it('returns feature flags and metadata', async () => {
22+
fetchOnce(listFeatureFlagsFixture);
23+
24+
const { data, listMetadata } =
25+
await workos.featureFlags.listFeatureFlags();
26+
27+
expect(fetchSearchParams()).toEqual({
28+
order: 'desc',
29+
});
30+
expect(fetchURL()).toContain('/feature-flags');
31+
32+
expect(data).toHaveLength(3);
33+
expect(data[0]).toEqual({
34+
object: 'feature_flag',
35+
id: 'flag_01EHQMYV6MBK39QC5PZXHY59C5',
36+
name: 'Advanced Dashboard',
37+
slug: 'advanced-dashboard',
38+
description: 'Enable advanced dashboard features',
39+
tags: ['ui'],
40+
enabled: true,
41+
defaultValue: false,
42+
createdAt: '2024-01-01T00:00:00.000Z',
43+
updatedAt: '2024-01-01T00:00:00.000Z',
44+
});
45+
46+
expect(listMetadata).toEqual({
47+
before: null,
48+
after: 'flag_01EHQMYV6MBK39QC5PZXHY59C7',
49+
});
50+
});
51+
});
52+
53+
describe('with the before option', () => {
54+
it('forms the proper request to the API', async () => {
55+
fetchOnce(listFeatureFlagsFixture);
56+
57+
const { data } = await workos.featureFlags.listFeatureFlags({
58+
before: 'flag_before_id',
59+
});
60+
61+
expect(fetchSearchParams()).toEqual({
62+
before: 'flag_before_id',
63+
order: 'desc',
64+
});
65+
66+
expect(fetchURL()).toContain('/feature-flags');
67+
expect(data).toHaveLength(3);
68+
});
69+
});
70+
71+
describe('with the after option', () => {
72+
it('forms the proper request to the API', async () => {
73+
fetchOnce(listFeatureFlagsFixture);
74+
75+
const { data } = await workos.featureFlags.listFeatureFlags({
76+
after: 'flag_after_id',
77+
});
78+
79+
expect(fetchSearchParams()).toEqual({
80+
after: 'flag_after_id',
81+
order: 'desc',
82+
});
83+
84+
expect(fetchURL()).toContain('/feature-flags');
85+
expect(data).toHaveLength(3);
86+
});
87+
});
88+
89+
describe('with the limit option', () => {
90+
it('forms the proper request to the API', async () => {
91+
fetchOnce(listFeatureFlagsFixture);
92+
93+
const { data } = await workos.featureFlags.listFeatureFlags({
94+
limit: 10,
95+
});
96+
97+
expect(fetchSearchParams()).toEqual({
98+
limit: '10',
99+
order: 'desc',
100+
});
101+
102+
expect(fetchURL()).toContain('/feature-flags');
103+
expect(data).toHaveLength(3);
104+
});
105+
});
106+
});
107+
108+
describe('getFeatureFlag', () => {
109+
it('requests a feature flag by slug', async () => {
110+
fetchOnce(getFeatureFlagFixture);
111+
112+
const subject = await workos.featureFlags.getFeatureFlag(
113+
'advanced-dashboard',
114+
);
115+
116+
expect(fetchURL()).toContain('/feature-flags/advanced-dashboard');
117+
expect(subject).toEqual({
118+
object: 'feature_flag',
119+
id: 'flag_01EHQMYV6MBK39QC5PZXHY59C5',
120+
name: 'Advanced Dashboard',
121+
slug: 'advanced-dashboard',
122+
description: 'Enable advanced dashboard features',
123+
tags: ['ui'],
124+
enabled: true,
125+
defaultValue: false,
126+
createdAt: '2024-01-01T00:00:00.000Z',
127+
updatedAt: '2024-01-01T00:00:00.000Z',
128+
});
129+
});
130+
});
131+
132+
describe('enableFeatureFlag', () => {
133+
it('enables a feature flag by slug', async () => {
134+
fetchOnce(enableFeatureFlagFixture);
135+
136+
const subject = await workos.featureFlags.enableFeatureFlag(
137+
'advanced-dashboard',
138+
);
139+
140+
expect(fetchURL()).toContain('/feature-flags/advanced-dashboard/enable');
141+
expect(fetchMethod()).toBe('PUT');
142+
expect(subject.enabled).toBe(true);
143+
});
144+
});
145+
146+
describe('disableFeatureFlag', () => {
147+
it('disables a feature flag by slug', async () => {
148+
fetchOnce(disableFeatureFlagFixture);
149+
150+
const subject = await workos.featureFlags.disableFeatureFlag(
151+
'advanced-dashboard',
152+
);
153+
154+
expect(fetchURL()).toContain('/feature-flags/advanced-dashboard/disable');
155+
expect(fetchMethod()).toBe('PUT');
156+
expect(subject.enabled).toBe(false);
157+
});
158+
});
159+
160+
describe('addFlagTarget', () => {
161+
it('adds a target to a feature flag', async () => {
162+
fetchOnce({}, { status: 204 });
163+
164+
await workos.featureFlags.addFlagTarget({
165+
slug: 'advanced-dashboard',
166+
targetId: 'user_01EHQMYV6MBK39QC5PZXHY59C5',
167+
});
168+
169+
expect(fetchURL()).toContain(
170+
'/feature-flags/advanced-dashboard/targets/user_01EHQMYV6MBK39QC5PZXHY59C5',
171+
);
172+
expect(fetchMethod()).toBe('POST');
173+
});
174+
175+
it('adds an organization target to a feature flag', async () => {
176+
fetchOnce({}, { status: 204 });
177+
178+
await workos.featureFlags.addFlagTarget({
179+
slug: 'advanced-dashboard',
180+
targetId: 'org_01EHQMYV6MBK39QC5PZXHY59C5',
181+
});
182+
183+
expect(fetchURL()).toContain(
184+
'/feature-flags/advanced-dashboard/targets/org_01EHQMYV6MBK39QC5PZXHY59C5',
185+
);
186+
expect(fetchMethod()).toBe('POST');
187+
});
188+
});
189+
190+
describe('removeFlagTarget', () => {
191+
it('removes a target from a feature flag', async () => {
192+
fetchOnce({}, { status: 204 });
193+
194+
await workos.featureFlags.removeFlagTarget({
195+
slug: 'advanced-dashboard',
196+
targetId: 'user_01EHQMYV6MBK39QC5PZXHY59C5',
197+
});
198+
199+
expect(fetchURL()).toContain(
200+
'/feature-flags/advanced-dashboard/targets/user_01EHQMYV6MBK39QC5PZXHY59C5',
201+
);
202+
expect(fetchMethod()).toBe('DELETE');
203+
});
204+
205+
it('removes an organization target from a feature flag', async () => {
206+
fetchOnce({}, { status: 204 });
207+
208+
await workos.featureFlags.removeFlagTarget({
209+
slug: 'advanced-dashboard',
210+
targetId: 'org_01EHQMYV6MBK39QC5PZXHY59C5',
211+
});
212+
213+
expect(fetchURL()).toContain(
214+
'/feature-flags/advanced-dashboard/targets/org_01EHQMYV6MBK39QC5PZXHY59C5',
215+
);
216+
expect(fetchMethod()).toBe('DELETE');
217+
});
218+
});
219+
});

src/feature-flags/feature-flags.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { AutoPaginatable } from '../common/utils/pagination';
2+
import { WorkOS } from '../workos';
3+
import {
4+
AddFlagTargetOptions,
5+
FeatureFlag,
6+
FeatureFlagResponse,
7+
ListFeatureFlagsOptions,
8+
RemoveFlagTargetOptions,
9+
} from './interfaces';
10+
import { deserializeFeatureFlag } from './serializers';
11+
import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize';
12+
13+
export class FeatureFlags {
14+
constructor(private readonly workos: WorkOS) {}
15+
16+
async listFeatureFlags(
17+
options?: ListFeatureFlagsOptions,
18+
): Promise<AutoPaginatable<FeatureFlag>> {
19+
return new AutoPaginatable(
20+
await fetchAndDeserialize<FeatureFlagResponse, FeatureFlag>(
21+
this.workos,
22+
'/feature-flags',
23+
deserializeFeatureFlag,
24+
options,
25+
),
26+
(params) =>
27+
fetchAndDeserialize<FeatureFlagResponse, FeatureFlag>(
28+
this.workos,
29+
'/feature-flags',
30+
deserializeFeatureFlag,
31+
params,
32+
),
33+
options,
34+
);
35+
}
36+
37+
async getFeatureFlag(slug: string): Promise<FeatureFlag> {
38+
const { data } = await this.workos.get<FeatureFlagResponse>(
39+
`/feature-flags/${slug}`,
40+
);
41+
42+
return deserializeFeatureFlag(data);
43+
}
44+
45+
async enableFeatureFlag(slug: string): Promise<FeatureFlag> {
46+
const { data } = await this.workos.put<FeatureFlagResponse>(
47+
`/feature-flags/${slug}/enable`,
48+
{},
49+
);
50+
51+
return deserializeFeatureFlag(data);
52+
}
53+
54+
async disableFeatureFlag(slug: string): Promise<FeatureFlag> {
55+
const { data } = await this.workos.put<FeatureFlagResponse>(
56+
`/feature-flags/${slug}/disable`,
57+
{},
58+
);
59+
60+
return deserializeFeatureFlag(data);
61+
}
62+
63+
async addFlagTarget(options: AddFlagTargetOptions): Promise<void> {
64+
const { slug, targetId } = options;
65+
await this.workos.post(`/feature-flags/${slug}/targets/${targetId}`, {});
66+
}
67+
68+
async removeFlagTarget(options: RemoveFlagTargetOptions): Promise<void> {
69+
const { slug, targetId } = options;
70+
await this.workos.delete(`/feature-flags/${slug}/targets/${targetId}`);
71+
}
72+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"object": "feature_flag",
3+
"id": "flag_01EHQMYV6MBK39QC5PZXHY59C5",
4+
"name": "Advanced Dashboard",
5+
"slug": "advanced-dashboard",
6+
"description": "Enable advanced dashboard features",
7+
"tags": ["ui"],
8+
"enabled": false,
9+
"default_value": false,
10+
"created_at": "2024-01-01T00:00:00.000Z",
11+
"updated_at": "2024-01-01T00:00:00.000Z"
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"object": "feature_flag",
3+
"id": "flag_01EHQMYV6MBK39QC5PZXHY59C5",
4+
"name": "Advanced Dashboard",
5+
"slug": "advanced-dashboard",
6+
"description": "Enable advanced dashboard features",
7+
"tags": ["ui"],
8+
"enabled": true,
9+
"default_value": false,
10+
"created_at": "2024-01-01T00:00:00.000Z",
11+
"updated_at": "2024-01-01T00:00:00.000Z"
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"object": "feature_flag",
3+
"id": "flag_01EHQMYV6MBK39QC5PZXHY59C5",
4+
"name": "Advanced Dashboard",
5+
"slug": "advanced-dashboard",
6+
"description": "Enable advanced dashboard features",
7+
"tags": ["ui"],
8+
"enabled": true,
9+
"default_value": false,
10+
"created_at": "2024-01-01T00:00:00.000Z",
11+
"updated_at": "2024-01-01T00:00:00.000Z"
12+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"object": "list",
3+
"data": [
4+
{
5+
"object": "feature_flag",
6+
"id": "flag_01EHQMYV6MBK39QC5PZXHY59C5",
7+
"name": "Advanced Dashboard",
8+
"slug": "advanced-dashboard",
9+
"description": "Enable advanced dashboard features",
10+
"tags": ["ui"],
11+
"enabled": true,
12+
"default_value": false,
13+
"created_at": "2024-01-01T00:00:00.000Z",
14+
"updated_at": "2024-01-01T00:00:00.000Z"
15+
},
16+
{
17+
"object": "feature_flag",
18+
"id": "flag_01EHQMYV6MBK39QC5PZXHY59C6",
19+
"name": "Beta Features",
20+
"slug": "beta-features",
21+
"description": "",
22+
"tags": [],
23+
"enabled": false,
24+
"default_value": false,
25+
"created_at": "2024-01-01T00:00:00.000Z",
26+
"updated_at": "2024-01-01T00:00:00.000Z"
27+
},
28+
{
29+
"object": "feature_flag",
30+
"id": "flag_01EHQMYV6MBK39QC5PZXHY59C7",
31+
"name": "Premium Support",
32+
"slug": "premium-support",
33+
"description": "Access to premium support features",
34+
"tags": ["dev-support"],
35+
"enabled": false,
36+
"default_value": true,
37+
"created_at": "2024-01-01T00:00:00.000Z",
38+
"updated_at": "2024-01-01T00:00:00.000Z"
39+
}
40+
],
41+
"list_metadata": {
42+
"before": null,
43+
"after": "flag_01EHQMYV6MBK39QC5PZXHY59C7"
44+
}
45+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface AddFlagTargetOptions {
2+
slug: string;
3+
targetId: string;
4+
}

0 commit comments

Comments
 (0)