1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-23 00:16:25 +01:00

feat: frontend API POST endpoint (#9291)

https://linear.app/unleash/issue/2-3260/implement-post-request-support-in-unleashs-frontend-api

Implements the POST endpoint in Unleash's frontend API.
This commit is contained in:
Nuno Góis 2025-02-12 11:56:51 +00:00 committed by GitHub
parent 5c23a52119
commit 151db95c2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 154 additions and 15 deletions

View File

@ -2,7 +2,7 @@
import crypto from 'crypto';
import type { Context } from 'unleash-client';
export function createContext(value: any): Context {
export function createContext(contextData: any): Context {
const {
appName,
environment,
@ -11,7 +11,7 @@ export function createContext(value: any): Context {
remoteAddress,
properties,
...rest
} = value;
} = contextData;
// move non root context fields to properties
const context: Context = {
@ -31,8 +31,9 @@ export function createContext(value: any): Context {
return cleanContext;
}
export const enrichContextWithIp = (query: any, ip: string): Context => {
query.remoteAddress = query.remoteAddress || ip;
query.sessionId = query.sessionId || crypto.randomBytes(18).toString('hex');
return createContext(query);
export const enrichContextWithIp = (contextData: any, ip: string): Context => {
contextData.remoteAddress = contextData.remoteAddress || ip;
contextData.sessionId =
contextData.sessionId || crypto.randomBytes(18).toString('hex');
return createContext(contextData);
};

View File

@ -83,8 +83,25 @@ export default class FrontendAPIController extends Controller {
this.route({
method: 'post',
path: '',
handler: FrontendAPIController.endpointNotImplemented,
handler: this.getFrontendApiFeatures,
permission: NONE,
middleware: [
this.services.openApiService.validPath({
tags: ['Frontend API'],
operationId: 'getFrontendApiFeaturesWithPost',
requestBody: createRequestSchema(
'frontendApiFeaturesPostSchema',
),
responses: {
200: createResponseSchema('frontendApiFeaturesSchema'),
...getStandardResponses(401, 404),
},
summary:
'Retrieve enabled feature flags for the provided context, using POST.',
description:
'This endpoint returns the list of feature flags that the frontend API evaluates to enabled for the given context, using POST. Context values are provided as a `context` property in the request body. If the Frontend API is disabled 404 is returned.',
}),
],
});
this.route({
@ -233,7 +250,11 @@ export default class FrontendAPIController extends Controller {
}
private static createContext(req: ApiUserRequest): Context {
const { query } = req;
return enrichContextWithIp(query, req.ip);
const { query, body } = req;
const bodyContext = body.context ?? {};
const contextData = req.method === 'POST' ? bodyContext : query;
return enrichContextWithIp(contextData, req.ip);
}
}

View File

@ -278,12 +278,6 @@ test('should allow requests with a frontend token', async () => {
test('should return 405 from unimplemented endpoints', async () => {
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
await app.request
.post('/api/frontend')
.send({})
.set('Authorization', frontendToken.secret)
.expect('Content-Type', /json/)
.expect(405);
await app.request
.get('/api/frontend/client/features')
.set('Authorization', frontendToken.secret)
@ -1234,3 +1228,103 @@ test('should resolve variable rollout percentage consistently', async () => {
}
}
});
test('should return enabled feature flags using POST', async () => {
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
await createFeatureToggle({
name: 'enabledFeature1',
enabled: true,
strategies: [{ name: 'default', constraints: [], parameters: {} }],
});
await createFeatureToggle({
name: 'enabledFeature2',
enabled: true,
strategies: [{ name: 'default', constraints: [], parameters: {} }],
});
await createFeatureToggle({
name: 'disabledFeature',
enabled: false,
strategies: [{ name: 'default', constraints: [], parameters: {} }],
});
await frontendApiService.refreshData();
await app.request
.post('/api/frontend')
.set('Authorization', frontendToken.secret)
.set('Content-Type', 'application/json')
.send()
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body).toEqual({
toggles: [
{
name: 'enabledFeature1',
enabled: true,
impressionData: false,
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
{
name: 'enabledFeature2',
enabled: true,
impressionData: false,
variant: {
enabled: false,
name: 'disabled',
feature_enabled: true,
featureEnabled: true,
},
},
],
});
});
});
test('should return enabled feature flags based on context using POST', async () => {
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
const featureName = 'featureWithEnvironmentConstraint';
await createFeatureToggle({
name: featureName,
enabled: true,
strategies: [
{
name: 'default',
constraints: [
{
contextName: 'userId',
operator: 'IN',
values: ['1337'],
},
],
parameters: {},
},
],
});
await frontendApiService.refreshData();
await app.request
.post('/api/frontend')
.set('Authorization', frontendToken.secret)
.set('Content-Type', 'application/json')
.send({ context: { userId: '1337' } })
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.toggles).toHaveLength(1);
expect(res.body.toggles[0].name).toBe(featureName);
});
await app.request
.post('/api/frontend')
.set('Authorization', frontendToken.secret)
.send({ context: { appName: 'test', userId: '42' } })
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.toggles).toHaveLength(0);
});
});

View File

@ -0,0 +1,22 @@
import type { FromSchema } from 'json-schema-to-ts';
import { sdkContextSchema } from './sdk-context-schema';
export const frontendApiFeaturesPostSchema = {
$id: '#/components/schemas/frontendApiFeaturesPostContextSchema',
description: 'The Unleash frontend API POST request body.',
type: 'object',
additionalProperties: true,
properties: {
context: {
description: 'The Unleash context.',
type: 'object',
additionalProperties: true,
properties: sdkContextSchema.properties,
},
},
components: {},
} as const;
export type FrontendApiFeaturesPostSchema = FromSchema<
typeof frontendApiFeaturesPostSchema
>;

View File

@ -101,6 +101,7 @@ export * from './feedback-response-schema';
export * from './feedback-update-schema';
export * from './frontend-api-client-schema';
export * from './frontend-api-feature-schema';
export * from './frontend-api-features-post-schema';
export * from './frontend-api-features-schema';
export * from './group-schema';
export * from './group-user-model-schema';