1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-04 01:18:20 +02:00

open api implementation - client features controller (#1745)

* open api implementation - client features controller

* open api implementation - client features controller

* bug fix

* test fix

* PR comments

* OAS for client-api metrics.ts

* Refactoring

* Refactoring

* bug fix

* fix PR comments

* PR comment

* PR comment
This commit is contained in:
andreas-unleash 2022-06-30 12:54:14 +03:00 committed by GitHub
parent b67aca8fbf
commit e875e67d24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 936 additions and 18 deletions

View File

@ -82,10 +82,14 @@ import { emailSchema } from './spec/email-schema';
import { strategySchema } from './spec/strategy-schema'; import { strategySchema } from './spec/strategy-schema';
import { strategiesSchema } from './spec/strategies-schema'; import { strategiesSchema } from './spec/strategies-schema';
import { upsertStrategySchema } from './spec/upsert-strategy-schema'; import { upsertStrategySchema } from './spec/upsert-strategy-schema';
import { clientFeaturesQuerySchema } from './spec/client-features-query-schema';
import { clientFeatureSchema } from './spec/client-feature-schema';
import { clientFeaturesSchema } from './spec/client-features-schema';
import { eventSchema } from './spec/event-schema'; import { eventSchema } from './spec/event-schema';
import { eventsSchema } from './spec/events-schema'; import { eventsSchema } from './spec/events-schema';
import { featureEventsSchema } from './spec/feature-events-schema'; import { featureEventsSchema } from './spec/feature-events-schema';
import { clientApplicationSchema } from './spec/client-application-schema'; import { clientApplicationSchema } from './spec/client-application-schema';
import { clientVariantSchema } from './spec/client-variant-schema';
import { IServerOption } from '../types'; import { IServerOption } from '../types';
import { URL } from 'url'; import { URL } from 'url';
@ -101,6 +105,10 @@ export const schemas = {
applicationsSchema, applicationsSchema,
clientApplicationSchema, clientApplicationSchema,
cloneFeatureSchema, cloneFeatureSchema,
clientFeatureSchema,
clientFeaturesSchema,
clientVariantSchema,
clientFeaturesQuerySchema,
changePasswordSchema, changePasswordSchema,
constraintSchema, constraintSchema,
contextFieldSchema, contextFieldSchema,

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`clientFeaturesSchema no fields 1`] = `
Object {
"errors": Array [
Object {
"instancePath": "",
"keyword": "required",
"message": "must have required property 'version'",
"params": Object {
"missingProperty": "version",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/clientFeaturesSchema",
}
`;

View File

@ -4,13 +4,13 @@ exports[`featureSchema constraints 1`] = `
Object { Object {
"errors": Array [ "errors": Array [
Object { Object {
"instancePath": "/strategies/0", "instancePath": "/strategies/0/constraints/0",
"keyword": "required", "keyword": "required",
"message": "must have required property 'id'", "message": "must have required property 'operator'",
"params": Object { "params": Object {
"missingProperty": "id", "missingProperty": "operator",
}, },
"schemaPath": "#/required", "schemaPath": "#/components/schemas/constraintSchema/required",
}, },
], ],
"schema": "#/components/schemas/featureSchema", "schema": "#/components/schemas/featureSchema",

View File

@ -0,0 +1,70 @@
import { FromSchema } from 'json-schema-to-ts';
import { constraintSchema } from './constraint-schema';
import { parametersSchema } from './parameters-schema';
import { featureStrategySchema } from './feature-strategy-schema';
import { clientVariantSchema } from './client-variant-schema';
export const clientFeatureSchema = {
$id: '#/components/schemas/clientFeatureSchema',
type: 'object',
required: ['name', 'enabled'],
additionalProperties: false,
properties: {
name: {
type: 'string',
},
type: {
type: 'string',
},
description: {
type: 'string',
nullable: true,
},
createdAt: {
type: 'string',
format: 'date-time',
nullable: true,
},
lastSeenAt: {
type: 'string',
format: 'date-time',
nullable: true,
},
enabled: {
type: 'boolean',
},
stale: {
type: 'boolean',
},
impressionData: {
type: 'boolean',
nullable: true,
},
project: {
type: 'string',
},
strategies: {
type: 'array',
items: {
$ref: '#/components/schemas/featureStrategySchema',
},
},
variants: {
type: 'array',
items: {
$ref: '#/components/schemas/clientVariantSchema',
},
nullable: true,
},
},
components: {
schemas: {
constraintSchema,
parametersSchema,
featureStrategySchema,
clientVariantSchema,
},
},
} as const;
export type ClientFeatureSchema = FromSchema<typeof clientFeatureSchema>;

View File

@ -0,0 +1,24 @@
import { validateSchema } from '../validate';
import { ClientFeaturesQuerySchema } from './client-features-query-schema';
test('clientFeatureQuerySchema empty', () => {
const data: ClientFeaturesQuerySchema = {};
expect(
validateSchema('#/components/schemas/clientFeaturesQuerySchema', data),
).toBeUndefined();
});
test('clientFeatureQuerySchema all fields', () => {
const data: ClientFeaturesQuerySchema = {
tag: [['some-tag', 'some-other-tag']],
project: ['default'],
namePrefix: 'some-prefix',
environment: 'some-env',
inlineSegmentConstraints: true,
};
expect(
validateSchema('#/components/schemas/clientFeaturesQuerySchema', data),
).toBeUndefined();
});

View File

@ -0,0 +1,39 @@
import { FromSchema } from 'json-schema-to-ts';
export const clientFeaturesQuerySchema = {
$id: '#/components/schemas/clientFeaturesQuerySchema',
type: 'object',
required: [],
additionalProperties: false,
properties: {
tag: {
type: 'array',
items: {
type: 'array',
items: {
type: 'string',
},
},
},
project: {
type: 'array',
items: {
type: 'string',
},
},
namePrefix: {
type: 'string',
},
environment: {
type: 'string',
},
inlineSegmentConstraints: {
type: 'boolean',
},
},
components: {},
} as const;
export type ClientFeaturesQuerySchema = FromSchema<
typeof clientFeaturesQuerySchema
>;

View File

@ -0,0 +1,395 @@
import { validateSchema } from '../validate';
import { ClientFeaturesSchema } from './client-features-schema';
test('clientFeaturesSchema no fields', () => {
expect(
validateSchema('#/components/schemas/clientFeaturesSchema', {}),
).toMatchSnapshot();
});
test('clientFeaturesSchema required fields', () => {
const data: ClientFeaturesSchema = {
version: 0,
query: {},
features: [
{
name: 'some-name',
enabled: false,
impressionData: false,
},
],
};
expect(
validateSchema('#/components/schemas/clientFeaturesSchema', data),
).toBeUndefined();
});
test('clientFeaturesSchema java-sdk expected response', () => {
const json = `{
"version": 2,
"segments": [
{
"id": 1,
"name": "some-name",
"description": null,
"constraints": [
{
"contextName": "some-name",
"operator": "IN",
"value": "name",
"inverted": false,
"caseInsensitive": true
}
]
}
],
"features": [
{
"name": "Test.old",
"description": "No variants here!",
"enabled": true,
"strategies": [
{
"name": "default"
}
],
"variants": null,
"createdAt": "2019-01-24T10:38:10.370Z"
},
{
"name": "Test.variants",
"description": null,
"enabled": true,
"strategies": [
{
"name": "default",
"segments": [
1
]
}
],
"variants": [
{
"name": "variant1",
"weight": 50
},
{
"name": "variant2",
"weight": 50
}
],
"createdAt": "2019-01-24T10:41:45.236Z"
},
{
"name": "featureX",
"enabled": true,
"strategies": [
{
"name": "default"
}
]
},
{
"name": "featureY",
"enabled": false,
"strategies": [
{
"name": "baz",
"parameters": {
"foo": "bar"
}
}
]
},
{
"name": "featureZ",
"enabled": true,
"strategies": [
{
"name": "default"
},
{
"name": "hola",
"parameters": {
"name": "val"
},
"segments": [1]
}
]
}
]
}
`;
expect(
validateSchema(
'#/components/schemas/clientFeaturesSchema',
JSON.parse(json),
),
).toBeUndefined();
});
test('clientFeaturesSchema unleash-proxy expected response', () => {
const json = `{
"version": 2,
"segments": [
{
"id": 1,
"name": "some-name",
"description": null,
"constraints": [
{
"contextName": "some-name",
"operator": "IN",
"value": "name",
"inverted": false,
"caseInsensitive": true
}
]
}
],
"features": [
{
"name": "Test.old",
"description": "No variants here!",
"enabled": true,
"strategies": [
{
"name": "default"
}
],
"variants": null,
"createdAt": "2019-01-24T10:38:10.370Z"
},
{
"name": "Test.variants",
"description": null,
"enabled": true,
"strategies": [
{
"name": "default",
"segments": [
1
]
}
],
"variants": [
{
"name": "variant1",
"weight": 50
},
{
"name": "variant2",
"weight": 50
}
],
"createdAt": "2019-01-24T10:41:45.236Z"
},
{
"name": "featureX",
"enabled": true,
"strategies": [
{
"name": "default"
}
]
},
{
"name": "featureY",
"enabled": false,
"strategies": [
{
"name": "baz",
"parameters": {
"foo": "bar"
}
}
]
},
{
"name": "featureZ",
"enabled": true,
"strategies": [
{
"name": "default"
},
{
"name": "hola",
"parameters": {
"name": "val"
},
"segments": [1]
}
]
}
]
}
`;
expect(
validateSchema(
'#/components/schemas/clientFeaturesSchema',
JSON.parse(json),
),
).toBeUndefined();
});
test('clientFeaturesSchema client specification test 15', () => {
const json = `{
"version": 2,
"features": [
{
"name": "F9.globalSegmentOn",
"description": "With global segment referencing constraint in on state",
"enabled": true,
"strategies": [
{
"name": "default",
"parameters": {},
"segments": [1]
}
]
},
{
"name": "F9.globalSegmentOff",
"description": "With global segment referencing constraint in off state",
"enabled": true,
"strategies": [
{
"name": "default",
"parameters": {},
"segments": [2]
}
]
},
{
"name": "F9.globalSegmentAndConstraint",
"description": "With global segment and constraint both on",
"enabled": true,
"strategies": [
{
"name": "default",
"parameters": {},
"constraints": [
{
"contextName": "version",
"operator": "SEMVER_EQ",
"value": "1.2.2"
}
],
"segments": [1]
}
]
},
{
"name": "F9.withExtraParams",
"description": "With global segment that doesn't exist",
"enabled": true,
"project": "some-project",
"strategies": [
{
"name": "default",
"parameters": {},
"constraints": [
{
"contextName": "version",
"operator": "SEMVER_EQ",
"value": "1.2.2"
}
],
"segments": [3]
}
]
},
{
"name": "F9.withSeveralConstraintsAndSegments",
"description": "With several segments and constraints",
"enabled": true,
"strategies": [
{
"name": "default",
"parameters": {},
"constraints": [
{
"contextName": "customNumber",
"operator": "NUM_LT",
"value": "10"
},
{
"contextName": "version",
"operator": "SEMVER_LT",
"value": "3.2.2"
}
],
"segments": [1, 4, 5]
}
]
}
],
"segments": [
{
"id": 1,
"constraints": [
{
"contextName": "version",
"operator": "SEMVER_EQ",
"value": "1.2.2"
}
]
},
{
"id": 2,
"constraints": [
{
"contextName": "version",
"operator": "SEMVER_EQ",
"value": "3.1.4"
}
]
},
{
"id": 3,
"constraints": [
{
"contextName": "version",
"operator": "SEMVER_EQ",
"value": "3.1.4"
}
]
},
{
"id": 4,
"constraints": [
{
"contextName": "customName",
"operator": "STR_CONTAINS",
"values": ["Pi"]
}
]
},
{
"id": 5,
"constraints": [
{
"contextName": "slicesLeft",
"operator": "NUM_LTE",
"value": "4"
}
]
}
]
}
`;
expect(
validateSchema(
'#/components/schemas/clientFeaturesSchema',
JSON.parse(json),
),
).toBeUndefined();
});

View File

@ -0,0 +1,51 @@
import { FromSchema } from 'json-schema-to-ts';
import { clientFeaturesQuerySchema } from './client-features-query-schema';
import { segmentSchema } from './segment-schema';
import { constraintSchema } from './constraint-schema';
import { environmentSchema } from './environment-schema';
import { overrideSchema } from './override-schema';
import { parametersSchema } from './parameters-schema';
import { featureStrategySchema } from './feature-strategy-schema';
import { variantSchema } from './variant-schema';
import { clientFeatureSchema } from './client-feature-schema';
export const clientFeaturesSchema = {
$id: '#/components/schemas/clientFeaturesSchema',
type: 'object',
required: ['version', 'features'],
properties: {
version: {
type: 'number',
},
features: {
type: 'array',
items: {
$ref: '#/components/schemas/clientFeatureSchema',
},
},
segments: {
type: 'array',
items: {
$ref: '#/components/schemas/segmentSchema',
},
},
query: {
$ref: '#/components/schemas/clientFeaturesQuerySchema',
},
},
components: {
schemas: {
constraintSchema,
clientFeatureSchema,
environmentSchema,
segmentSchema,
clientFeaturesQuerySchema,
overrideSchema,
parametersSchema,
featureStrategySchema,
variantSchema,
},
},
} as const;
export type ClientFeaturesSchema = FromSchema<typeof clientFeaturesSchema>;

View File

@ -0,0 +1,31 @@
import { FromSchema } from 'json-schema-to-ts';
export const clientVariantSchema = {
$id: '#/components/schemas/clientVariantSchema',
type: 'object',
additionalProperties: false,
required: ['name', 'weight'],
properties: {
name: {
type: 'string',
},
weight: {
type: 'number',
},
payload: {
type: 'object',
required: ['type', 'value'],
properties: {
type: {
type: 'string',
},
value: {
type: 'string',
},
},
},
},
components: {},
} as const;
export type ClientVariantSchema = FromSchema<typeof clientVariantSchema>;

View File

@ -6,7 +6,7 @@ export const featureStrategySchema = {
$id: '#/components/schemas/featureStrategySchema', $id: '#/components/schemas/featureStrategySchema',
type: 'object', type: 'object',
additionalProperties: false, additionalProperties: false,
required: ['name', 'id'], required: ['name'],
properties: { properties: {
id: { id: {
type: 'string', type: 'string',

View File

@ -5,13 +5,17 @@ export const segmentSchema = {
$id: '#/components/schemas/segmentSchema', $id: '#/components/schemas/segmentSchema',
type: 'object', type: 'object',
additionalProperties: false, additionalProperties: false,
required: ['name', 'constraints'], required: ['id', 'constraints'],
properties: { properties: {
id: {
type: 'number',
},
name: { name: {
type: 'string', type: 'string',
}, },
description: { description: {
type: 'string', type: 'string',
nullable: true,
}, },
constraints: { constraints: {
type: 'array', type: 'array',

View File

@ -70,7 +70,10 @@ test('should get empty getFeatures via client', () => {
test('if caching is enabled should memoize', async () => { test('if caching is enabled should memoize', async () => {
const getClientFeatures = jest.fn().mockReturnValue([]); const getClientFeatures = jest.fn().mockReturnValue([]);
const getActive = jest.fn().mockReturnValue([]); const getActive = jest.fn().mockReturnValue([]);
const respondWithValidation = jest.fn().mockReturnValue({});
const validPath = jest.fn().mockReturnValue(jest.fn());
const clientSpecService = new ClientSpecService({ getLogger }); const clientSpecService = new ClientSpecService({ getLogger });
const openApiService = { respondWithValidation, validPath };
const featureToggleServiceV2 = { getClientFeatures }; const featureToggleServiceV2 = { getClientFeatures };
const segmentService = { getActive }; const segmentService = { getActive };
@ -78,6 +81,8 @@ test('if caching is enabled should memoize', async () => {
{ {
clientSpecService, clientSpecService,
// @ts-expect-error // @ts-expect-error
openApiService,
// @ts-expect-error
featureToggleServiceV2, featureToggleServiceV2,
// @ts-expect-error // @ts-expect-error
segmentService, segmentService,
@ -99,14 +104,19 @@ test('if caching is enabled should memoize', async () => {
test('if caching is not enabled all calls goes to service', async () => { test('if caching is not enabled all calls goes to service', async () => {
const getClientFeatures = jest.fn().mockReturnValue([]); const getClientFeatures = jest.fn().mockReturnValue([]);
const getActive = jest.fn().mockReturnValue([]); const getActive = jest.fn().mockReturnValue([]);
const respondWithValidation = jest.fn().mockReturnValue({});
const validPath = jest.fn().mockReturnValue(jest.fn());
const clientSpecService = new ClientSpecService({ getLogger }); const clientSpecService = new ClientSpecService({ getLogger });
const featureToggleServiceV2 = { getClientFeatures }; const featureToggleServiceV2 = { getClientFeatures };
const segmentService = { getActive }; const segmentService = { getActive };
const openApiService = { respondWithValidation, validPath };
const controller = new FeatureController( const controller = new FeatureController(
{ {
clientSpecService, clientSpecService,
// @ts-expect-error // @ts-expect-error
openApiService,
// @ts-expect-error
featureToggleServiceV2, featureToggleServiceV2,
// @ts-expect-error // @ts-expect-error
segmentService, segmentService,

View File

@ -1,8 +1,7 @@
import memoizee from 'memoizee'; import memoizee from 'memoizee';
import { Response } from 'express'; import { Response } from 'express';
import Controller from '../controller'; import Controller from '../controller';
import { IUnleashServices } from '../../types/services'; import { IUnleashConfig, IUnleashServices } from '../../types';
import { IUnleashConfig } from '../../types/option';
import FeatureToggleService from '../../services/feature-toggle-service'; import FeatureToggleService from '../../services/feature-toggle-service';
import { Logger } from '../../logger'; import { Logger } from '../../logger';
import { querySchema } from '../../schema/feature-schema'; import { querySchema } from '../../schema/feature-schema';
@ -14,6 +13,18 @@ import { ALL, isAllProjects } from '../../types/models/api-token';
import { SegmentService } from '../../services/segment-service'; import { SegmentService } from '../../services/segment-service';
import { FeatureConfigurationClient } from '../../types/stores/feature-strategies-store'; import { FeatureConfigurationClient } from '../../types/stores/feature-strategies-store';
import { ClientSpecService } from '../../services/client-spec-service'; import { ClientSpecService } from '../../services/client-spec-service';
import { OpenApiService } from '../../services/openapi-service';
import { NONE } from '../../types/permissions';
import { createResponseSchema } from '../../openapi';
import { ClientFeaturesQuerySchema } from '../../openapi/spec/client-features-query-schema';
import {
clientFeatureSchema,
ClientFeatureSchema,
} from '../../openapi/spec/client-feature-schema';
import {
clientFeaturesSchema,
ClientFeaturesSchema,
} from '../../openapi/spec/client-features-schema';
const version = 2; const version = 2;
@ -31,6 +42,8 @@ export default class FeatureController extends Controller {
private clientSpecService: ClientSpecService; private clientSpecService: ClientSpecService;
private openApiService: OpenApiService;
private readonly cache: boolean; private readonly cache: boolean;
private cachedFeatures: any; private cachedFeatures: any;
@ -40,9 +53,13 @@ export default class FeatureController extends Controller {
featureToggleServiceV2, featureToggleServiceV2,
segmentService, segmentService,
clientSpecService, clientSpecService,
openApiService,
}: Pick< }: Pick<
IUnleashServices, IUnleashServices,
'featureToggleServiceV2' | 'segmentService' | 'clientSpecService' | 'featureToggleServiceV2'
| 'segmentService'
| 'clientSpecService'
| 'openApiService'
>, >,
config: IUnleashConfig, config: IUnleashConfig,
) { ) {
@ -51,10 +68,40 @@ export default class FeatureController extends Controller {
this.featureToggleServiceV2 = featureToggleServiceV2; this.featureToggleServiceV2 = featureToggleServiceV2;
this.segmentService = segmentService; this.segmentService = segmentService;
this.clientSpecService = clientSpecService; this.clientSpecService = clientSpecService;
this.openApiService = openApiService;
this.logger = config.getLogger('client-api/feature.js'); this.logger = config.getLogger('client-api/feature.js');
this.get('/', this.getAll); this.route({
this.get('/:featureName', this.getFeatureToggle); method: 'get',
path: '/:featureName',
handler: this.getFeatureToggle,
permission: NONE,
middleware: [
openApiService.validPath({
operationId: 'getClientFeature',
tags: ['client'],
responses: {
200: createResponseSchema('clientFeaturesSchema'),
},
}),
],
});
this.route({
method: 'get',
path: '',
handler: this.getAll,
permission: NONE,
middleware: [
openApiService.validPath({
operationId: 'getAllClientFeatures',
tags: ['client'],
responses: {
200: createResponseSchema('clientFeaturesSchema'),
},
}),
],
});
if (clientFeatureCaching?.enabled) { if (clientFeatureCaching?.enabled) {
this.cache = true; this.cache = true;
@ -148,7 +195,10 @@ export default class FeatureController extends Controller {
return query; return query;
} }
async getAll(req: IAuthRequest, res: Response): Promise<void> { async getAll(
req: IAuthRequest,
res: Response<ClientFeaturesSchema>,
): Promise<void> {
const query = await this.resolveQuery(req); const query = await this.resolveQuery(req);
const [features, segments] = this.cache const [features, segments] = this.cache
@ -156,13 +206,26 @@ export default class FeatureController extends Controller {
: await this.resolveFeaturesAndSegments(query); : await this.resolveFeaturesAndSegments(query);
if (this.clientSpecService.requestSupportsSpec(req, 'segments')) { if (this.clientSpecService.requestSupportsSpec(req, 'segments')) {
res.json({ version, features, query, segments }); this.openApiService.respondWithValidation(
200,
res,
clientFeaturesSchema.$id,
{ version, features, query: { ...query }, segments },
);
} else { } else {
res.json({ version, features, query }); this.openApiService.respondWithValidation(
200,
res,
clientFeaturesSchema.$id,
{ version, features, query },
);
} }
} }
async getFeatureToggle(req: IAuthRequest, res: Response): Promise<void> { async getFeatureToggle(
req: IAuthRequest<{ featureName: string }, ClientFeaturesQuerySchema>,
res: Response<ClientFeatureSchema>,
): Promise<void> {
const name = req.params.featureName; const name = req.params.featureName;
const featureQuery = await this.resolveQuery(req); const featureQuery = await this.resolveQuery(req);
const q = { ...featureQuery, namePrefix: name }; const q = { ...featureQuery, namePrefix: name };
@ -172,6 +235,13 @@ export default class FeatureController extends Controller {
if (!toggle) { if (!toggle) {
throw new NotFoundError(`Could not find feature toggle ${name}`); throw new NotFoundError(`Could not find feature toggle ${name}`);
} }
res.json(toggle).end(); this.openApiService.respondWithValidation(
200,
res,
clientFeatureSchema.$id,
{
...toggle,
},
);
} }
} }

View File

@ -352,6 +352,151 @@ Object {
], ],
"type": "object", "type": "object",
}, },
"clientFeatureSchema": Object {
"additionalProperties": false,
"properties": Object {
"createdAt": Object {
"format": "date-time",
"nullable": true,
"type": "string",
},
"description": Object {
"nullable": true,
"type": "string",
},
"enabled": Object {
"type": "boolean",
},
"impressionData": Object {
"nullable": true,
"type": "boolean",
},
"lastSeenAt": Object {
"format": "date-time",
"nullable": true,
"type": "string",
},
"name": Object {
"type": "string",
},
"project": Object {
"type": "string",
},
"stale": Object {
"type": "boolean",
},
"strategies": Object {
"items": Object {
"$ref": "#/components/schemas/featureStrategySchema",
},
"type": "array",
},
"type": Object {
"type": "string",
},
"variants": Object {
"items": Object {
"$ref": "#/components/schemas/clientVariantSchema",
},
"nullable": true,
"type": "array",
},
},
"required": Array [
"name",
"enabled",
],
"type": "object",
},
"clientFeaturesQuerySchema": Object {
"additionalProperties": false,
"properties": Object {
"environment": Object {
"type": "string",
},
"inlineSegmentConstraints": Object {
"type": "boolean",
},
"namePrefix": Object {
"type": "string",
},
"project": Object {
"items": Object {
"type": "string",
},
"type": "array",
},
"tag": Object {
"items": Object {
"items": Object {
"type": "string",
},
"type": "array",
},
"type": "array",
},
},
"required": Array [],
"type": "object",
},
"clientFeaturesSchema": Object {
"properties": Object {
"features": Object {
"items": Object {
"$ref": "#/components/schemas/clientFeatureSchema",
},
"type": "array",
},
"query": Object {
"$ref": "#/components/schemas/clientFeaturesQuerySchema",
},
"segments": Object {
"items": Object {
"$ref": "#/components/schemas/segmentSchema",
},
"type": "array",
},
"version": Object {
"type": "number",
},
},
"required": Array [
"version",
"features",
],
"type": "object",
},
"clientVariantSchema": Object {
"additionalProperties": false,
"properties": Object {
"name": Object {
"type": "string",
},
"payload": Object {
"properties": Object {
"type": Object {
"type": "string",
},
"value": Object {
"type": "string",
},
},
"required": Array [
"type",
"value",
],
"type": "object",
},
"weight": Object {
"type": "number",
},
},
"required": Array [
"name",
"weight",
],
"type": "object",
},
"cloneFeatureSchema": Object { "cloneFeatureSchema": Object {
"properties": Object { "properties": Object {
"name": Object { "name": Object {
@ -952,7 +1097,6 @@ Object {
}, },
"required": Array [ "required": Array [
"name", "name",
"id",
], ],
"type": "object", "type": "object",
}, },
@ -1537,14 +1681,18 @@ Object {
"type": "array", "type": "array",
}, },
"description": Object { "description": Object {
"nullable": true,
"type": "string", "type": "string",
}, },
"id": Object {
"type": "number",
},
"name": Object { "name": Object {
"type": "string", "type": "string",
}, },
}, },
"required": Array [ "required": Array [
"name", "id",
"constraints", "constraints",
], ],
"type": "object", "type": "object",
@ -5266,6 +5414,56 @@ If the provided project does not exist, the list of events will be empty.",
], ],
}, },
}, },
"/api/client/features": Object {
"get": Object {
"operationId": "getAllClientFeatures",
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/clientFeaturesSchema",
},
},
},
"description": "clientFeaturesSchema",
},
},
"tags": Array [
"client",
],
},
},
"/api/client/features/{featureName}": Object {
"get": Object {
"operationId": "getClientFeature",
"parameters": Array [
Object {
"in": "path",
"name": "featureName",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/clientFeaturesSchema",
},
},
},
"description": "clientFeaturesSchema",
},
},
"tags": Array [
"client",
],
},
},
"/api/client/register": Object { "/api/client/register": Object {
"post": Object { "post": Object {
"operationId": "registerClientApplication", "operationId": "registerClientApplication",