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:
parent
5c23a52119
commit
151db95c2d
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
22
src/lib/openapi/spec/frontend-api-features-post-schema.ts
Normal file
22
src/lib/openapi/spec/frontend-api-features-post-schema.ts
Normal 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
|
||||
>;
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user