mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-26 01:17:00 +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:
parent
b67aca8fbf
commit
e875e67d24
@ -82,10 +82,14 @@ import { emailSchema } from './spec/email-schema';
|
||||
import { strategySchema } from './spec/strategy-schema';
|
||||
import { strategiesSchema } from './spec/strategies-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 { eventsSchema } from './spec/events-schema';
|
||||
import { featureEventsSchema } from './spec/feature-events-schema';
|
||||
import { clientApplicationSchema } from './spec/client-application-schema';
|
||||
import { clientVariantSchema } from './spec/client-variant-schema';
|
||||
import { IServerOption } from '../types';
|
||||
import { URL } from 'url';
|
||||
|
||||
@ -101,6 +105,10 @@ export const schemas = {
|
||||
applicationsSchema,
|
||||
clientApplicationSchema,
|
||||
cloneFeatureSchema,
|
||||
clientFeatureSchema,
|
||||
clientFeaturesSchema,
|
||||
clientVariantSchema,
|
||||
clientFeaturesQuerySchema,
|
||||
changePasswordSchema,
|
||||
constraintSchema,
|
||||
contextFieldSchema,
|
||||
|
@ -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",
|
||||
}
|
||||
`;
|
@ -4,13 +4,13 @@ exports[`featureSchema constraints 1`] = `
|
||||
Object {
|
||||
"errors": Array [
|
||||
Object {
|
||||
"instancePath": "/strategies/0",
|
||||
"instancePath": "/strategies/0/constraints/0",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'id'",
|
||||
"message": "must have required property 'operator'",
|
||||
"params": Object {
|
||||
"missingProperty": "id",
|
||||
"missingProperty": "operator",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
"schemaPath": "#/components/schemas/constraintSchema/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/featureSchema",
|
||||
|
70
src/lib/openapi/spec/client-feature-schema.ts
Normal file
70
src/lib/openapi/spec/client-feature-schema.ts
Normal 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>;
|
24
src/lib/openapi/spec/client-features-query-schema.test.ts
Normal file
24
src/lib/openapi/spec/client-features-query-schema.test.ts
Normal 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();
|
||||
});
|
39
src/lib/openapi/spec/client-features-query-schema.ts
Normal file
39
src/lib/openapi/spec/client-features-query-schema.ts
Normal 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
|
||||
>;
|
395
src/lib/openapi/spec/client-features-schema.test.ts
Normal file
395
src/lib/openapi/spec/client-features-schema.test.ts
Normal 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();
|
||||
});
|
51
src/lib/openapi/spec/client-features-schema.ts
Normal file
51
src/lib/openapi/spec/client-features-schema.ts
Normal 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>;
|
31
src/lib/openapi/spec/client-variant-schema.ts
Normal file
31
src/lib/openapi/spec/client-variant-schema.ts
Normal 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>;
|
@ -6,7 +6,7 @@ export const featureStrategySchema = {
|
||||
$id: '#/components/schemas/featureStrategySchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['name', 'id'],
|
||||
required: ['name'],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
|
@ -5,13 +5,17 @@ export const segmentSchema = {
|
||||
$id: '#/components/schemas/segmentSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['name', 'constraints'],
|
||||
required: ['id', 'constraints'],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'number',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
constraints: {
|
||||
type: 'array',
|
||||
|
@ -70,7 +70,10 @@ test('should get empty getFeatures via client', () => {
|
||||
test('if caching is enabled should memoize', async () => {
|
||||
const getClientFeatures = 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 openApiService = { respondWithValidation, validPath };
|
||||
const featureToggleServiceV2 = { getClientFeatures };
|
||||
const segmentService = { getActive };
|
||||
|
||||
@ -78,6 +81,8 @@ test('if caching is enabled should memoize', async () => {
|
||||
{
|
||||
clientSpecService,
|
||||
// @ts-expect-error
|
||||
openApiService,
|
||||
// @ts-expect-error
|
||||
featureToggleServiceV2,
|
||||
// @ts-expect-error
|
||||
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 () => {
|
||||
const getClientFeatures = 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 featureToggleServiceV2 = { getClientFeatures };
|
||||
const segmentService = { getActive };
|
||||
const openApiService = { respondWithValidation, validPath };
|
||||
|
||||
const controller = new FeatureController(
|
||||
{
|
||||
clientSpecService,
|
||||
// @ts-expect-error
|
||||
openApiService,
|
||||
// @ts-expect-error
|
||||
featureToggleServiceV2,
|
||||
// @ts-expect-error
|
||||
segmentService,
|
||||
|
@ -1,8 +1,7 @@
|
||||
import memoizee from 'memoizee';
|
||||
import { Response } from 'express';
|
||||
import Controller from '../controller';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { IUnleashConfig, IUnleashServices } from '../../types';
|
||||
import FeatureToggleService from '../../services/feature-toggle-service';
|
||||
import { Logger } from '../../logger';
|
||||
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 { FeatureConfigurationClient } from '../../types/stores/feature-strategies-store';
|
||||
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;
|
||||
|
||||
@ -31,6 +42,8 @@ export default class FeatureController extends Controller {
|
||||
|
||||
private clientSpecService: ClientSpecService;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
private readonly cache: boolean;
|
||||
|
||||
private cachedFeatures: any;
|
||||
@ -40,9 +53,13 @@ export default class FeatureController extends Controller {
|
||||
featureToggleServiceV2,
|
||||
segmentService,
|
||||
clientSpecService,
|
||||
openApiService,
|
||||
}: Pick<
|
||||
IUnleashServices,
|
||||
'featureToggleServiceV2' | 'segmentService' | 'clientSpecService'
|
||||
| 'featureToggleServiceV2'
|
||||
| 'segmentService'
|
||||
| 'clientSpecService'
|
||||
| 'openApiService'
|
||||
>,
|
||||
config: IUnleashConfig,
|
||||
) {
|
||||
@ -51,10 +68,40 @@ export default class FeatureController extends Controller {
|
||||
this.featureToggleServiceV2 = featureToggleServiceV2;
|
||||
this.segmentService = segmentService;
|
||||
this.clientSpecService = clientSpecService;
|
||||
this.openApiService = openApiService;
|
||||
this.logger = config.getLogger('client-api/feature.js');
|
||||
|
||||
this.get('/', this.getAll);
|
||||
this.get('/:featureName', this.getFeatureToggle);
|
||||
this.route({
|
||||
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) {
|
||||
this.cache = true;
|
||||
@ -148,7 +195,10 @@ export default class FeatureController extends Controller {
|
||||
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 [features, segments] = this.cache
|
||||
@ -156,13 +206,26 @@ export default class FeatureController extends Controller {
|
||||
: await this.resolveFeaturesAndSegments(query);
|
||||
|
||||
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 {
|
||||
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 featureQuery = await this.resolveQuery(req);
|
||||
const q = { ...featureQuery, namePrefix: name };
|
||||
@ -172,6 +235,13 @@ export default class FeatureController extends Controller {
|
||||
if (!toggle) {
|
||||
throw new NotFoundError(`Could not find feature toggle ${name}`);
|
||||
}
|
||||
res.json(toggle).end();
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
clientFeatureSchema.$id,
|
||||
{
|
||||
...toggle,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -352,6 +352,151 @@ 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 {
|
||||
"properties": Object {
|
||||
"name": Object {
|
||||
@ -952,7 +1097,6 @@ Object {
|
||||
},
|
||||
"required": Array [
|
||||
"name",
|
||||
"id",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
@ -1537,14 +1681,18 @@ Object {
|
||||
"type": "array",
|
||||
},
|
||||
"description": Object {
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"id": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"name": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"name",
|
||||
"id",
|
||||
"constraints",
|
||||
],
|
||||
"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 {
|
||||
"post": Object {
|
||||
"operationId": "registerClientApplication",
|
||||
|
Loading…
Reference in New Issue
Block a user