1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-17 01:17:29 +02:00

Complete open api schemas for project features controller (#1563)

* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)

* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)

* bug fix

* bug fix

* fix merge conflicts, some refactoring

* fix merge conflicts, some refactoring

* fix merge conflicts, some refactoring

* added emptyResponse, patch feature operation schemas and request

* added emptyResponse, patch feature operation schemas and request

* patch strategy

* patch strategy

* update strategy

* update strategy

* fix pr comment

* fix pr comments

* improvements

* added operationId to schema for better generation

* fix pr comment

* fix pr comment

* fix pr comment

* improvements to generated and dynamic types

* improvements to generated and dynamic types

* improvements to generated and dynamic types

* Update response types to use inferred types

* Update addTag response status to 201

* refactor: move schema ref destructuring into createSchemaObject

* made serialize date handle deep objects

* made serialize date handle deep objects

* add `name` to IFeatureStrategy nad fix tests

* fix pr comments

* fix pr comments

* Add types to IAuthRequest

* Sync StrategySchema for FE and BE - into the rabbit hole

* Sync model with OAS spec

* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)

* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)

* bug fix

* bug fix

* fix merge conflicts, some refactoring

* fix merge conflicts, some refactoring

* fix merge conflicts, some refactoring

* added emptyResponse, patch feature operation schemas and request

* added emptyResponse, patch feature operation schemas and request

* patch strategy

* patch strategy

* update strategy

* update strategy

* fix pr comment

* fix pr comments

* improvements

* added operationId to schema for better generation

* fix pr comment

* fix pr comment

* fix pr comment

* improvements to generated and dynamic types

* improvements to generated and dynamic types

* improvements to generated and dynamic types

* Update response types to use inferred types

* Update addTag response status to 201

* refactor: move schema ref destructuring into createSchemaObject

* made serialize date handle deep objects

* made serialize date handle deep objects

* add `name` to IFeatureStrategy nad fix tests

* fix pr comments

* fix pr comments

* Add types to IAuthRequest

* Sync StrategySchema for FE and BE - into the rabbit hole

* Sync model with OAS spec

* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)

* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)

* bug fix

* bug fix

* fix merge conflicts, some refactoring

* fix merge conflicts, some refactoring

* fix merge conflicts, some refactoring

* added emptyResponse, patch feature operation schemas and request

* added emptyResponse, patch feature operation schemas and request

* patch strategy

* patch strategy

* update strategy

* update strategy

* fix pr comment

* fix pr comments

* improvements

* added operationId to schema for better generation

* fix pr comment

* fix pr comment

* fix pr comment

* improvements to generated and dynamic types

* improvements to generated and dynamic types

* improvements to generated and dynamic types

* Update response types to use inferred types

* Update addTag response status to 201

* refactor: move schema ref destructuring into createSchemaObject

* made serialize date handle deep objects

* made serialize date handle deep objects

* add `name` to IFeatureStrategy nad fix tests

* fix pr comments

* fix pr comments

* Add types to IAuthRequest

* Sync StrategySchema for FE and BE - into the rabbit hole

* Sync model with OAS spec

* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)

* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)

* bug fix

* bug fix

* fix merge conflicts, some refactoring

* fix merge conflicts, some refactoring

* fix merge conflicts, some refactoring

* added emptyResponse, patch feature operation schemas and request

* added emptyResponse, patch feature operation schemas and request

* patch strategy

* patch strategy

* update strategy

* update strategy

* fix pr comment

* fix pr comments

* improvements

* added operationId to schema for better generation

* fix pr comment

* fix pr comment

* fix pr comment

* improvements to generated and dynamic types

* improvements to generated and dynamic types

* improvements to generated and dynamic types

* Update response types to use inferred types

* Update addTag response status to 201

* refactor: move schema ref destructuring into createSchemaObject

* made serialize date handle deep objects

* made serialize date handle deep objects

* add `name` to IFeatureStrategy nad fix tests

* fix pr comments

* fix pr comments

* Add types to IAuthRequest

* Sync StrategySchema for FE and BE - into the rabbit hole

* Sync model with OAS spec

* revert

* revert

* revert

* revert

* revert

* mapper

* revert

* revert

* revert

* remove serialize-dates.ts

* remove serialize-dates.ts

* remove serialize-dates.ts

* remove serialize-dates.ts

* remove serialize-dates.ts

* revert

* revert

* add mappers

* add mappers

* fix pr comments

* ignore report.json

* ignore report.json

* Route permission required

Co-authored-by: olav <mail@olav.io>
This commit is contained in:
andreas-unleash 2022-05-18 16:17:09 +03:00 committed by GitHub
parent 7d1a5c2012
commit 1a27bffe4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1916 additions and 539 deletions

1
.gitignore vendored
View File

@ -54,3 +54,4 @@ package-lock.json
/website/i18n/*
.env
report.json

View File

@ -282,7 +282,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
env.strategies = [];
}
if (r.strategy_id) {
env.strategies.push(this.getAdminStrategy(r));
env.strategies.push(
FeatureStrategiesStore.getAdminStrategy(r),
);
}
acc.environments[r.environment] = env;
return acc;
@ -310,7 +312,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
private getEnvironment(r: any): IEnvironmentOverview {
private static getEnvironment(r: any): IEnvironmentOverview {
return {
name: r.environment,
enabled: r.enabled,
@ -350,7 +352,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
const overview = rows.reduce((acc, r) => {
if (acc[r.feature_name] !== undefined) {
acc[r.feature_name].environments.push(
this.getEnvironment(r),
FeatureStrategiesStore.getEnvironment(r),
);
} else {
acc[r.feature_name] = {
@ -359,7 +361,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
createdAt: r.created_at,
lastSeenAt: r.last_seen_at,
stale: r.stale,
environments: [this.getEnvironment(r)],
environments: [
FeatureStrategiesStore.getEnvironment(r),
],
};
}
return acc;
@ -399,7 +403,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
return mapRow(row[0]);
}
private getAdminStrategy(
private static getAdminStrategy(
r: any,
includeId: boolean = true,
): IStrategyConfig {

View File

@ -10,8 +10,8 @@ import {
import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store';
import { DEFAULT_ENV } from '../util/constants';
import { PartialDeep } from '../types/partial';
import { EventEmitter } from 'stream';
import { IExperimentalOptions } from '../experimental';
import EventEmitter from 'events';
export interface FeaturesTable {
name: string;
@ -149,7 +149,9 @@ export default class FeatureToggleClientStore
strategies: [],
};
if (this.isUnseenStrategyRow(feature, r)) {
feature.strategies.push(this.rowToStrategy(r));
feature.strategies.push(
FeatureToggleClientStore.rowToStrategy(r),
);
}
if (inlineSegmentConstraints && r.segment_id) {
this.addSegmentToStrategy(feature, r);
@ -176,13 +178,13 @@ export default class FeatureToggleClientStore
if (!isAdmin) {
// We should not send strategy IDs from the client API,
// as this breaks old versions of the Go SDK (at least).
this.removeIdsFromStrategies(features);
FeatureToggleClientStore.removeIdsFromStrategies(features);
}
return features;
}
private rowToStrategy(row: Record<string, any>): IStrategyConfig {
private static rowToStrategy(row: Record<string, any>): IStrategyConfig {
return {
id: row.strategy_id,
name: row.strategy_name,
@ -191,7 +193,7 @@ export default class FeatureToggleClientStore
};
}
private removeIdsFromStrategies(features: IFeatureToggleClient[]) {
private static removeIdsFromStrategies(features: IFeatureToggleClient[]) {
features.forEach((feature) => {
feature.strategies.forEach((strategy) => {
delete strategy.id;

View File

@ -1,13 +1,22 @@
import { OpenAPIV3 } from 'openapi-types';
import { constraintSchema } from './spec/constraint-schema';
import { createFeatureSchema } from './spec/create-feature-schema';
import { createStrategySchema } from './spec/create-strategy-schema';
import { featureSchema } from './spec/feature-schema';
import { featuresSchema } from './spec/features-schema';
import { overrideSchema } from './spec/override-schema';
import { parametersSchema } from './spec/parameters-schema';
import { strategySchema } from './spec/strategy-schema';
import { variantSchema } from './spec/variant-schema';
import { createFeatureSchema } from './spec/create-feature-schema';
import { constraintSchema } from './spec/constraint-schema';
import { tagSchema } from './spec/tag-schema';
import { tagsResponseSchema } from './spec/tags-response-schema';
import { createStrategySchema } from './spec/create-strategy-schema';
import { featureSchema } from './spec/feature-schema';
import { parametersSchema } from './spec/parameters-schema';
import { featureEnvironmentInfoSchema } from './spec/feature-environment-info-schema';
import { emptyResponseSchema } from './spec/empty-response-schema';
import { patchOperationSchema } from './spec/patch-operation-schema';
import { updateFeatureSchema } from './spec/updateFeatureSchema';
import { updateStrategySchema } from './spec/update-strategy-schema';
import { cloneFeatureSchema } from './spec/clone-feature-schema';
import { featureStrategySchema } from './spec/feature-strategy-schema';
export const createOpenApiSchema = (
serverUrl?: string,
@ -34,14 +43,23 @@ export const createOpenApiSchema = (
},
schemas: {
constraintSchema,
cloneFeatureSchema,
createFeatureSchema,
createStrategySchema,
featureSchema,
featuresSchema,
featureEnvironmentInfoSchema,
featureStrategySchema,
emptyResponseSchema,
overrideSchema,
parametersSchema,
patchOperationSchema,
strategySchema,
updateStrategySchema,
updateFeatureSchema,
variantSchema,
tagSchema,
tagsResponseSchema,
},
},
};

View File

@ -0,0 +1,29 @@
import { SchemaMapper } from './mapper';
import { IFeatureEnvironmentInfo } from '../../types/model';
import { FeatureEnvironmentInfoSchema } from '../spec/feature-environment-info-schema';
import { FeatureStrategyMapper } from './feature-strategy.mapper';
export class EnvironmentInfoMapper
implements
SchemaMapper<
FeatureEnvironmentInfoSchema,
IFeatureEnvironmentInfo,
Partial<FeatureEnvironmentInfoSchema>
>
{
private mapper = new FeatureStrategyMapper();
fromPublic(input: FeatureEnvironmentInfoSchema): IFeatureEnvironmentInfo {
return {
...input,
strategies: input.strategies.map(this.mapper.fromPublic),
};
}
toPublic(input: IFeatureEnvironmentInfo): FeatureEnvironmentInfoSchema {
return {
...input,
strategies: input.strategies.map(this.mapper.toPublic),
};
}
}

View File

@ -0,0 +1,29 @@
import { SchemaMapper } from './mapper';
import { IFeatureStrategy } from '../../types/model';
import { CreateStrategySchema } from '../spec/create-strategy-schema';
import { UpdateStrategySchema } from '../spec/update-strategy-schema';
import { FeatureStrategySchema } from '../spec/feature-strategy-schema';
export class FeatureStrategyMapper
implements
SchemaMapper<
FeatureStrategySchema,
IFeatureStrategy,
CreateStrategySchema | UpdateStrategySchema
>
{
fromPublic(input: FeatureStrategySchema): IFeatureStrategy {
return {
...input,
id: input.id || '',
projectId: input.projectId! || '',
};
}
toPublic(input: IFeatureStrategy): FeatureStrategySchema {
return {
...input,
name: input.strategyName,
};
}
}

View File

@ -0,0 +1,3 @@
export * from './environment-info.mapper';
export * from './feature-strategy.mapper';
export * from './strategy.mapper';

View File

@ -0,0 +1,7 @@
// Convert between public schema types and internal data types.
// Avoids coupling public schemas to internal implementation details.
export interface SchemaMapper<SCHEMA, INTERNAL, INPUT = Partial<SCHEMA>> {
fromPublic(input: SCHEMA): INTERNAL;
toPublic(input: INTERNAL): SCHEMA;
mapInput?(input: INPUT): INTERNAL;
}

View File

@ -0,0 +1,33 @@
import { SchemaMapper } from './mapper';
import { IStrategyConfig } from '../../types/model';
import { StrategySchema } from '../spec/strategy-schema';
import { CreateStrategySchema } from '../spec/create-strategy-schema';
import { UpdateStrategySchema } from '../spec/update-strategy-schema';
export class StrategyMapper
implements
SchemaMapper<
StrategySchema,
IStrategyConfig,
CreateStrategySchema | UpdateStrategySchema
>
{
fromPublic(input: StrategySchema): IStrategyConfig {
return input;
}
toPublic(input: IStrategyConfig): StrategySchema {
return input;
}
mapInput(
input: CreateStrategySchema | UpdateStrategySchema,
): IStrategyConfig {
return {
...input,
name: input.name || '',
parameters: input.parameters || {},
constraints: input.constraints || [],
};
}
}

View File

@ -0,0 +1,12 @@
import { OpenAPIV3 } from 'openapi-types';
export const cloneFeatureRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/cloneFeatureSchema',
},
},
},
};

View File

@ -0,0 +1,19 @@
import { createSchemaObject, CreateSchemaType } from '../types';
const schema = {
type: 'object',
required: ['name', 'replaceGroupId'],
properties: {
name: {
type: 'string',
},
replaceGroupId: {
type: 'boolean',
},
},
'components/schemas': {},
} as const;
export type CloneFeatureSchema = CreateSchemaType<typeof schema>;
export const cloneFeatureSchema = createSchemaObject(schema);

View File

@ -1,4 +1,5 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { ALL_OPERATORS } from '../../util/constants';
const schema = {
type: 'object',
@ -10,6 +11,7 @@ const schema = {
},
operator: {
type: 'string',
enum: ALL_OPERATORS,
},
caseInsensitive: {
type: 'boolean',
@ -27,6 +29,7 @@ const schema = {
type: 'string',
},
},
'components/schemas': {},
} as const;
export type ConstraintSchema = CreateSchemaType<typeof schema>;

View File

@ -17,6 +17,7 @@ const schema = {
type: 'boolean',
},
},
'components/schemas': {},
} as const;
export type CreateFeatureSchema = CreateSchemaType<typeof schema>;

View File

@ -14,9 +14,15 @@ const schema = {
},
constraints: {
type: 'array',
items: constraintSchema,
items: { $ref: '#/components/schemas/constraintSchema' },
},
parameters: parametersSchema,
parameters: {
$ref: '#/components/schemas/parametersSchema',
},
},
'components/schemas': {
constraintSchema,
parametersSchema,
},
} as const;

View File

@ -0,0 +1,12 @@
import { OpenAPIV3 } from 'openapi-types';
export const createTagRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/tagSchema',
},
},
},
};

View File

@ -0,0 +1,11 @@
import { createSchemaObject, CreateSchemaType } from '../types';
const schema = {
type: 'object',
description: 'OK',
'components/schemas': {},
} as const;
export type EmptyResponseSchema = CreateSchemaType<typeof schema>;
export const emptyResponseSchema = createSchemaObject(schema);

View File

@ -0,0 +1,12 @@
import { OpenAPIV3 } from 'openapi-types';
export const emptyResponse: OpenAPIV3.ResponseObject = {
description: 'emptyResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/emptyResponseSchema',
},
},
},
};

View File

@ -0,0 +1,12 @@
import { OpenAPIV3 } from 'openapi-types';
export const featureEnvironmentInfoResponse: OpenAPIV3.ResponseObject = {
description: 'featureEnvironmentInfoResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/featureEnvironmentInfoSchema',
},
},
},
};

View File

@ -0,0 +1,35 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { featureStrategySchema } from './feature-strategy-schema';
let schema = {
type: 'object',
additionalProperties: false,
required: ['name', 'environment', 'enabled', 'strategies'],
properties: {
name: {
type: 'string',
},
environment: {
type: 'string',
},
type: {
type: 'string',
},
enabled: {
type: 'boolean',
},
strategies: {
type: 'array',
items: {
$ref: '#/components/schemas/featureStrategySchema',
},
},
},
'components/schemas': {
featureStrategySchema,
},
} as const;
export type FeatureEnvironmentInfoSchema = CreateSchemaType<typeof schema>;
export const featureEnvironmentInfoSchema = createSchemaObject(schema);

View File

@ -1,6 +1,7 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { strategySchema } from './strategy-schema';
import { variantSchema } from './variant-schema';
import { featureEnvironmentInfoSchema } from './feature-environment-info-schema';
const schema = {
type: 'object',
@ -16,6 +17,9 @@ const schema = {
description: {
type: 'string',
},
archived: {
type: 'boolean',
},
project: {
type: 'string',
},
@ -38,15 +42,28 @@ const schema = {
format: 'date',
nullable: true,
},
environments: {
type: 'array',
items: {
$ref: '#/components/schemas/featureEnvironmentInfoSchema',
},
},
strategies: {
type: 'array',
items: strategySchema,
items: { $ref: '#/components/schemas/strategySchema' },
},
variants: {
items: variantSchema,
type: 'array',
items: {
$ref: '#/components/schemas/variantSchema',
},
},
},
'components/schemas': {
featureEnvironmentInfoSchema,
strategySchema,
variantSchema,
},
} as const;
export type FeatureSchema = CreateSchemaType<typeof schema>;

View File

@ -0,0 +1,58 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { constraintSchema } from './constraint-schema';
import { parametersSchema } from './parameters-schema';
export const schema = {
type: 'object',
additionalProperties: false,
required: [
'id',
'name',
'featureName',
'strategyName',
'constraints',
'parameters',
'environment',
],
properties: {
id: {
type: 'string',
},
name: {
type: 'string',
},
createdAt: {
type: 'string',
format: 'date',
nullable: true,
},
featureName: {
type: 'string',
},
projectId: {
type: 'string',
},
environment: {
type: 'string',
},
strategyName: {
type: 'string',
},
sortOrder: {
type: 'number',
},
constraints: {
type: 'array',
items: { $ref: '#/components/schemas/constraintSchema' },
},
parameters: { $ref: '#/components/schemas/parametersSchema' },
},
'components/schemas': {
constraintSchema,
parametersSchema,
},
} as const;
export type FeatureStrategySchema = CreateSchemaType<typeof schema>;
export const featureStrategySchema = createSchemaObject(schema);

View File

@ -11,9 +11,12 @@ const schema = {
},
features: {
type: 'array',
items: featureSchema,
items: { $ref: '#/components/schemas/featureSchema' },
},
},
'components/schemas': {
featureSchema: { schema: featureSchema },
},
} as const;
export type FeaturesSchema = CreateSchemaType<typeof schema>;

View File

@ -15,6 +15,7 @@ const schema = {
},
},
},
'components/schemas': {},
} as const;
export type OverrideSchema = CreateSchemaType<typeof schema>;

View File

@ -6,6 +6,7 @@ const schema = {
type: 'string',
maxLength: 100,
},
'components/schemas': {},
} as const;
export type ParametersSchema = CreateSchemaType<typeof schema>;

View File

@ -0,0 +1,24 @@
import { createSchemaObject, CreateSchemaType } from '../types';
const schema = {
type: 'object',
required: ['path', 'op'],
properties: {
path: {
type: 'string',
},
op: {
type: 'string',
enum: ['add', 'remove', 'replace', 'copy', 'move'],
},
from: {
type: 'string',
},
value: {},
},
'components/schemas': {},
} as const;
export type PatchOperationSchema = CreateSchemaType<typeof schema>;
export const patchOperationSchema = createSchemaObject(schema);

View File

@ -0,0 +1,15 @@
import { OpenAPIV3 } from 'openapi-types';
export const patchRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/patchOperationSchema',
},
},
},
},
};

View File

@ -0,0 +1,15 @@
import { OpenAPIV3 } from 'openapi-types';
export const strategiesResponse: OpenAPIV3.ResponseObject = {
description: 'strategiesResponse',
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/strategySchema',
},
},
},
},
};

View File

@ -5,7 +5,7 @@ export const strategyResponse: OpenAPIV3.ResponseObject = {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/strategySchema',
$ref: '#/components/schemas/featureStrategySchema',
},
},
},

View File

@ -2,10 +2,10 @@ import { createSchemaObject, CreateSchemaType } from '../types';
import { constraintSchema } from './constraint-schema';
import { parametersSchema } from './parameters-schema';
const schema = {
export const strategySchemaDefinition = {
type: 'object',
additionalProperties: false,
required: ['id', 'name', 'constraints', 'parameters'],
required: ['name', 'constraints', 'parameters'],
properties: {
id: {
type: 'string',
@ -13,14 +13,21 @@ const schema = {
name: {
type: 'string',
},
sortOrder: {
type: 'number',
},
constraints: {
type: 'array',
items: constraintSchema,
items: { $ref: '#/components/schemas/constraintSchema' },
},
parameters: parametersSchema,
parameters: { $ref: '#/components/schemas/parametersSchema' },
},
'components/schemas': {
constraintSchema,
parametersSchema,
},
} as const;
export type StrategySchema = CreateSchemaType<typeof schema>;
export type StrategySchema = CreateSchemaType<typeof strategySchemaDefinition>;
export const strategySchema = createSchemaObject(schema);
export const strategySchema = createSchemaObject(strategySchemaDefinition);

View File

@ -0,0 +1,12 @@
import { OpenAPIV3 } from 'openapi-types';
export const tagResponse: OpenAPIV3.ResponseObject = {
description: 'tagResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/tagSchema',
},
},
},
};

View File

@ -0,0 +1,20 @@
import { createSchemaObject, CreateSchemaType } from '../types';
const schema = {
type: 'object',
additionalProperties: false,
required: ['value', 'type'],
properties: {
value: {
type: 'string',
},
type: {
type: 'string',
},
},
'components/schemas': {},
} as const;
export type TagSchema = CreateSchemaType<typeof schema>;
export const tagSchema = createSchemaObject(schema);

View File

@ -0,0 +1,26 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { tagSchema } from './tag-schema';
const schema = {
type: 'object',
additionalProperties: false,
required: ['version', 'tags'],
properties: {
version: {
type: 'integer',
},
tags: {
type: 'array',
items: {
$ref: '#/components/schemas/tagSchema',
},
},
},
'components/schemas': {
tagSchema,
},
} as const;
export type TagsResponseSchema = CreateSchemaType<typeof schema>;
export const tagsResponseSchema = createSchemaObject(schema);

View File

@ -0,0 +1,12 @@
import { OpenAPIV3 } from 'openapi-types';
export const tagsResponse: OpenAPIV3.ResponseObject = {
description: 'tagsResponse',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/tagsResponseSchema',
},
},
},
};

View File

@ -0,0 +1,12 @@
import { OpenAPIV3 } from 'openapi-types';
export const updateFeatureRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/updateFeatureSchema',
},
},
},
};

View File

@ -0,0 +1,12 @@
import { OpenAPIV3 } from 'openapi-types';
export const updateStrategyRequest: OpenAPIV3.RequestBodyObject = {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/updateStrategySchema',
},
},
},
};

View File

@ -0,0 +1,11 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { strategySchemaDefinition } from './strategy-schema';
const schema = {
...strategySchemaDefinition,
required: [],
} as const;
export type UpdateStrategySchema = CreateSchemaType<typeof schema>;
export const updateStrategySchema = createSchemaObject(schema);

View File

@ -0,0 +1,42 @@
import { createSchemaObject, CreateSchemaType } from '../types';
import { constraintSchema } from './constraint-schema';
const schema = {
type: 'object',
required: ['name'],
properties: {
name: {
type: 'string',
},
description: {
type: 'string',
},
type: {
type: 'string',
},
stale: {
type: 'boolean',
},
archived: {
type: 'boolean',
},
createdAt: {
type: 'string',
format: 'date',
},
impressionData: {
type: 'boolean',
},
constraints: {
type: 'array',
items: { $ref: '#/components/schemas/constraintSchema' },
},
},
'components/schemas': {
constraintSchema,
},
} as const;
export type UpdateFeatureSchema = CreateSchemaType<typeof schema>;
export const updateFeatureSchema = createSchemaObject(schema);

View File

@ -20,12 +20,24 @@ const schema = {
},
payload: {
type: 'object',
required: ['type', 'value'],
properties: {
type: {
type: 'string',
},
value: {
type: 'string',
},
},
},
overrides: {
type: 'array',
items: overrideSchema,
items: { $ref: '#/components/schemas/overrideSchema' },
},
},
'components/schemas': {
overrideSchema,
},
} as const;
export type VariantSchema = CreateSchemaType<typeof schema>;

View File

@ -15,7 +15,31 @@ export interface ClientApiOperation
}
// Create a type from a const schema object.
export type CreateSchemaType<T> = FromSchema<T>;
export type CreateSchemaType<T> = FromSchema<
T,
{
definitionsPath: 'components/schemas';
deserialize: [
{
pattern: {
type: 'string';
format: 'date';
};
output: Date;
},
];
}
>;
// Create an OpenAPIV3.SchemaObject from a const schema object.
export const createSchemaObject = <T>(schema: T): DeepMutable<T> => schema;
// Make sure the schema contains an object of refs for type generation.
// Pass an empty 'components/schemas' object if there are no refs in the schema.
// Note: The order of the refs must match the order they are present in the object
export const createSchemaObject = <
T extends { 'components/schemas': { [key: string]: object } },
>(
schema: T,
): DeepMutable<Omit<T, 'components/schemas'>> => {
const { 'components/schemas': schemas, ...rest } = schema;
return rest;
};

View File

@ -1,17 +1,16 @@
import { Request, Response } from 'express';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import { IUnleashServices } from '../../types';
import { Logger } from '../../logger';
import Controller from '../controller';
import { extractUsername } from '../../util/extract-user';
import { DELETE_FEATURE, UPDATE_FEATURE } from '../../types/permissions';
import { DELETE_FEATURE, NONE, UPDATE_FEATURE } from '../../types/permissions';
import FeatureToggleService from '../../services/feature-toggle-service';
import { IAuthRequest } from '../unleash-types';
import { featuresResponse } from '../../openapi/spec/features-response';
import { FeaturesSchema } from '../../openapi/spec/features-schema';
import { serializeDates } from '../../util/serialize-dates';
export default class ArchiveController extends Controller {
private readonly logger: Logger;
@ -34,6 +33,7 @@ export default class ArchiveController extends Controller {
path: '/features',
acceptAnyContentType: true,
handler: this.getArchivedFeatures,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
@ -48,6 +48,7 @@ export default class ArchiveController extends Controller {
path: '/features/:projectId',
acceptAnyContentType: true,
handler: this.getArchivedFeaturesByProjectId,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
@ -75,7 +76,7 @@ export default class ArchiveController extends Controller {
res.json({
version: 2,
features: features.map(serializeDates),
features: features,
});
}
@ -91,7 +92,7 @@ export default class ArchiveController extends Controller {
);
res.json({
version: 2,
features: features.map(serializeDates),
features: features,
});
}

View File

@ -5,13 +5,13 @@ import Controller from '../controller';
import { extractUsername } from '../../util/extract-user';
import {
UPDATE_FEATURE,
DELETE_FEATURE,
CREATE_FEATURE,
DELETE_FEATURE,
NONE,
UPDATE_FEATURE,
} from '../../types/permissions';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import { IUnleashServices } from '../../types';
import FeatureToggleService from '../../services/feature-toggle-service';
import { featureSchema, querySchema } from '../../schema/feature-schema';
import { IFeatureToggleQuery } from '../../types/model';
@ -20,7 +20,12 @@ import { IAuthRequest } from '../unleash-types';
import { DEFAULT_ENV } from '../../util/constants';
import { featuresResponse } from '../../openapi/spec/features-response';
import { FeaturesSchema } from '../../openapi/spec/features-schema';
import { serializeDates } from '../../util/serialize-dates';
import { tagsResponse } from '../../openapi/spec/tags-response';
import { tagResponse } from '../../openapi/spec/tag-response';
import { createTagRequest } from '../../openapi/spec/create-tag-request';
import { emptyResponse } from '../../openapi/spec/empty-response';
import { TagSchema } from '../../openapi/spec/tag-schema';
import { TagsResponseSchema } from '../../openapi/spec/tags-response-schema';
const version = 1;
@ -66,23 +71,75 @@ class FeatureController extends Controller {
path: '',
acceptAnyContentType: true,
handler: this.getAllToggles,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getAllToggles',
responses: { 200: featuresResponse },
deprecated: true,
}),
],
});
this.post('/validate', this.validate, NONE);
this.get('/:featureName/tags', this.listTags);
this.post('/:featureName/tags', this.addTag, UPDATE_FEATURE);
this.delete(
'/:featureName/tags/:type/:value',
this.removeTag,
UPDATE_FEATURE,
);
this.route({
method: 'post',
path: '/validate',
handler: this.validate,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'validateFeature',
responses: { 200: emptyResponse },
}),
],
});
this.route({
method: 'get',
path: '/:featureName/tags',
handler: this.listTags,
acceptAnyContentType: true,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'listTags',
responses: { 200: tagsResponse },
}),
],
});
this.route({
method: 'post',
path: '/:featureName/tags',
permission: UPDATE_FEATURE,
handler: this.addTag,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'addTag',
requestBody: createTagRequest,
responses: { 201: tagResponse },
}),
],
});
this.route({
method: 'delete',
path: '/:featureName/tags/:type/:value',
permission: UPDATE_FEATURE,
acceptAnyContentType: true,
handler: this.removeTag,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'removeTag',
responses: { 200: emptyResponse },
}),
],
});
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -120,10 +177,9 @@ class FeatureController extends Controller {
): Promise<void> {
const query = await this.prepQuery(req.query);
const features = await this.service.getFeatureToggles(query);
res.json({
version,
features: features.map(serializeDates),
features: features,
});
}
@ -136,12 +192,23 @@ class FeatureController extends Controller {
res.json(feature).end();
}
async listTags(req: Request, res: Response): Promise<void> {
async listTags(
req: Request<{ featureName: string }, any, any, any>,
res: Response<TagsResponseSchema>,
): Promise<void> {
const tags = await this.tagService.listTags(req.params.featureName);
res.json({ version, tags });
}
async addTag(req: IAuthRequest, res: Response): Promise<void> {
async addTag(
req: IAuthRequest<
{ featureName: string },
Response<TagSchema>,
TagSchema,
any
>,
res: Response<TagSchema>,
): Promise<void> {
const { featureName } = req.params;
const userName = extractUsername(req);
const tag = await this.tagService.addTag(
@ -153,14 +220,20 @@ class FeatureController extends Controller {
}
// TODO
async removeTag(req: IAuthRequest, res: Response): Promise<void> {
async removeTag(
req: IAuthRequest<{ featureName: string; type: string; value: string }>,
res: Response<void>,
): Promise<void> {
const { featureName, type, value } = req.params;
const userName = extractUsername(req);
await this.tagService.removeTag(featureName, { type, value }, userName);
res.status(200).end();
}
async validate(req: Request, res: Response): Promise<void> {
async validate(
req: Request<any, any, { name: string }, any>,
res: Response<void>,
): Promise<void> {
const { name } = req.body;
await this.service.validateName(name);

View File

@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import { applyPatch, Operation } from 'fast-json-patch';
import Controller from '../../controller';
import { IUnleashConfig } from '../../../types/option';
import { IUnleashServices } from '../../../types/services';
import { IUnleashServices } from '../../../types';
import FeatureToggleService from '../../../services/feature-toggle-service';
import { Logger } from '../../../logger';
import {
@ -10,23 +10,38 @@ import {
CREATE_FEATURE_STRATEGY,
DELETE_FEATURE,
DELETE_FEATURE_STRATEGY,
NONE,
UPDATE_FEATURE,
UPDATE_FEATURE_ENVIRONMENT,
UPDATE_FEATURE_STRATEGY,
} from '../../../types/permissions';
import { FeatureToggleDTO, IStrategyConfig } from '../../../types/model';
import { extractUsername } from '../../../util/extract-user';
import { IAuthRequest } from '../../unleash-types';
import { createFeatureRequest } from '../../../openapi/spec/create-feature-request';
import { featureResponse } from '../../../openapi/spec/feature-response';
import { CreateFeatureSchema } from '../../../openapi/spec/create-feature-schema';
import { FeatureSchema } from '../../../openapi/spec/feature-schema';
import { serializeDates } from '../../../util/serialize-dates';
import { createStrategyRequest } from '../../../openapi/spec/create-strategy-request';
import { CreateStrategySchema } from '../../../openapi/spec/create-strategy-schema';
import { strategyResponse } from '../../../openapi/spec/strategy-response';
import { StrategySchema } from '../../../openapi/spec/strategy-schema';
import { featuresResponse } from '../../../openapi/spec/features-response';
import { featureEnvironmentInfoResponse } from '../../../openapi/spec/feature-environment-info-response';
import { strategiesResponse } from '../../../openapi/spec/strategies-response';
import { strategyResponse } from '../../../openapi/spec/strategy-response';
import { emptyResponse } from '../../../openapi/spec/empty-response';
import { updateFeatureRequest } from '../../../openapi/spec/update-feature-request';
import { patchRequest } from '../../../openapi/spec/patch-request';
import { updateStrategyRequest } from '../../../openapi/spec/update-strategy-request';
import { cloneFeatureRequest } from '../../../openapi/spec/clone-feature-request';
import { FeatureEnvironmentInfoSchema } from '../../../openapi/spec/feature-environment-info-schema';
import { ParametersSchema } from '../../../openapi/spec/parameters-schema';
import { FeaturesSchema } from '../../../openapi/spec/features-schema';
import { UpdateFeatureSchema } from '../../../openapi/spec/updateFeatureSchema';
import { UpdateStrategySchema } from '../../../openapi/spec/update-strategy-schema';
import { CreateStrategySchema } from '../../../openapi/spec/create-strategy-schema';
import {
EnvironmentInfoMapper,
StrategyMapper,
} from '../../../openapi/mappers';
interface FeatureStrategyParams {
projectId: string;
@ -62,6 +77,11 @@ type ProjectFeaturesServices = Pick<
export default class ProjectFeaturesController extends Controller {
private featureService: FeatureToggleService;
private strategyMapper: StrategyMapper = new StrategyMapper();
private environmentMapper: EnvironmentInfoMapper =
new EnvironmentInfoMapper();
private readonly logger: Logger;
constructor(
@ -72,21 +92,63 @@ export default class ProjectFeaturesController extends Controller {
this.featureService = featureToggleServiceV2;
this.logger = config.getLogger('/admin-api/project/features.ts');
this.get(`${PATH_ENV}`, this.getEnvironment);
this.route({
method: 'get',
path: PATH_ENV,
acceptAnyContentType: true,
permission: NONE,
handler: this.getEnvironment,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getEnvironment',
responses: { 200: featureEnvironmentInfoResponse },
}),
],
});
this.post(
`${PATH_ENV}/on`,
this.toggleEnvironmentOn,
UPDATE_FEATURE_ENVIRONMENT,
);
this.route({
method: 'post',
path: `${PATH_ENV}/off`,
handler: this.toggleEnvironmentOff,
permission: UPDATE_FEATURE_ENVIRONMENT,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'toggleEnvironmentOff',
responses: { 200: featureResponse },
}),
],
});
this.post(
`${PATH_ENV}/off`,
this.toggleEnvironmentOff,
UPDATE_FEATURE_ENVIRONMENT,
);
this.route({
method: 'post',
path: `${PATH_ENV}/on`,
handler: this.toggleEnvironmentOn,
permission: UPDATE_FEATURE_ENVIRONMENT,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'toggleEnvironmentOn',
responses: { 200: featureResponse },
}),
],
});
this.get(`${PATH_STRATEGIES}`, this.getStrategies);
this.route({
method: 'get',
path: PATH_STRATEGIES,
handler: this.getStrategies,
acceptAnyContentType: true,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getStrategies',
responses: { 200: strategiesResponse },
}),
],
});
this.route({
method: 'post',
@ -96,13 +158,27 @@ export default class ProjectFeaturesController extends Controller {
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'addStrategy',
requestBody: createStrategyRequest,
responses: { 200: strategyResponse },
}),
],
});
this.get(`${PATH_STRATEGY}`, this.getStrategy);
this.route({
method: 'get',
path: PATH_STRATEGY,
handler: this.getStrategy,
acceptAnyContentType: true,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getStrategy',
responses: { 200: strategyResponse },
}),
],
});
this.route({
method: 'put',
@ -112,39 +188,56 @@ export default class ProjectFeaturesController extends Controller {
middleware: [
openApiService.validPath({
tags: ['admin'],
requestBody: createStrategyRequest,
operationId: 'updateStrategy',
requestBody: updateStrategyRequest,
responses: { 200: strategyResponse },
}),
],
});
this.patch(
`${PATH_STRATEGY}`,
this.patchStrategy,
UPDATE_FEATURE_STRATEGY,
);
this.delete(
`${PATH_STRATEGY}`,
this.deleteStrategy,
DELETE_FEATURE_STRATEGY,
);
this.route({
method: 'patch',
path: PATH_STRATEGY,
handler: this.patchStrategy,
permission: UPDATE_FEATURE_STRATEGY,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'patchStrategy',
requestBody: patchRequest,
responses: { 200: strategyResponse },
}),
],
});
this.route({
method: 'delete',
path: PATH_STRATEGY,
acceptAnyContentType: true,
handler: this.deleteStrategy,
permission: DELETE_FEATURE_STRATEGY,
middleware: [
openApiService.validPath({
operationId: 'deleteStrategy',
tags: ['admin'],
responses: { 200: emptyResponse },
}),
],
});
this.route({
method: 'get',
path: PATH,
acceptAnyContentType: true,
handler: this.getFeatures,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getFeatures',
responses: { 200: featuresResponse },
}),
],
});
this.post(PATH_FEATURE_CLONE, this.cloneFeature, CREATE_FEATURE);
this.route({
method: 'post',
path: PATH,
@ -153,33 +246,95 @@ export default class ProjectFeaturesController extends Controller {
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'createFeature',
requestBody: createFeatureRequest,
responses: { 200: featureResponse },
}),
],
});
this.route({
method: 'post',
path: PATH_FEATURE_CLONE,
acceptAnyContentType: true,
handler: this.cloneFeature,
permission: CREATE_FEATURE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'cloneFeature',
requestBody: cloneFeatureRequest,
responses: { 200: featureResponse },
}),
],
});
this.route({
method: 'get',
path: PATH_FEATURE,
acceptAnyContentType: true,
handler: this.getFeature,
permission: NONE,
middleware: [
openApiService.validPath({
operationId: 'getFeature',
tags: ['admin'],
responses: { 200: featureResponse },
}),
],
});
this.put(PATH_FEATURE, this.updateFeature, UPDATE_FEATURE);
this.patch(PATH_FEATURE, this.patchFeature, UPDATE_FEATURE);
this.delete(PATH_FEATURE, this.archiveFeature, DELETE_FEATURE);
this.route({
method: 'put',
path: PATH_FEATURE,
acceptAnyContentType: true,
handler: this.updateFeature,
permission: UPDATE_FEATURE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'updateFeature',
requestBody: updateFeatureRequest,
responses: { 200: featureResponse },
}),
],
});
this.route({
method: 'patch',
path: PATH_FEATURE,
acceptAnyContentType: true,
handler: this.patchFeature,
permission: UPDATE_FEATURE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'patchFeature',
requestBody: patchRequest,
responses: { 200: featureResponse },
}),
],
});
this.route({
method: 'delete',
path: PATH_FEATURE,
acceptAnyContentType: true,
handler: this.archiveFeature,
permission: DELETE_FEATURE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'archiveFeature',
responses: { 200: emptyResponse },
}),
],
});
}
async getFeatures(
req: Request<ProjectParam, any, any, any>,
res: Response,
res: Response<FeaturesSchema>,
): Promise<void> {
const { projectId } = req.params;
const features = await this.featureService.getFeatureOverview(
@ -192,10 +347,10 @@ export default class ProjectFeaturesController extends Controller {
req: IAuthRequest<
FeatureParams,
any,
{ name: string; replaceGroupId: boolean },
{ name: string; replaceGroupId?: boolean },
any
>,
res: Response,
res: Response<FeatureSchema>,
): Promise<void> {
const { projectId, featureName } = req.params;
const { name, replaceGroupId } = req.body;
@ -204,7 +359,7 @@ export default class ProjectFeaturesController extends Controller {
featureName,
projectId,
name,
replaceGroupId,
Boolean(replaceGroupId),
userName,
);
res.status(201).json(created);
@ -223,7 +378,7 @@ export default class ProjectFeaturesController extends Controller {
userName,
);
res.status(201).json(serializeDates(created));
res.status(201).json(created);
}
async getFeature(
@ -239,10 +394,10 @@ export default class ProjectFeaturesController extends Controller {
req: IAuthRequest<
{ projectId: string; featureName: string },
any,
FeatureToggleDTO,
UpdateFeatureSchema,
any
>,
res: Response,
res: Response<FeatureSchema>,
): Promise<void> {
const { projectId, featureName } = req.params;
const data = req.body;
@ -263,7 +418,7 @@ export default class ProjectFeaturesController extends Controller {
Operation[],
any
>,
res: Response,
res: Response<FeatureSchema>,
): Promise<void> {
const { projectId, featureName } = req.params;
const updated = await this.featureService.patchFeature(
@ -283,7 +438,7 @@ export default class ProjectFeaturesController extends Controller {
any,
any
>,
res: Response,
res: Response<void>,
): Promise<void> {
const { featureName } = req.params;
const userName = extractUsername(req);
@ -293,7 +448,7 @@ export default class ProjectFeaturesController extends Controller {
async getEnvironment(
req: Request<FeatureStrategyParams, any, any, any>,
res: Response,
res: Response<FeatureEnvironmentInfoSchema>,
): Promise<void> {
const { environment, featureName, projectId } = req.params;
const environmentInfo = await this.featureService.getEnvironmentInfo(
@ -301,12 +456,12 @@ export default class ProjectFeaturesController extends Controller {
environment,
featureName,
);
res.status(200).json(environmentInfo);
res.status(200).json(this.environmentMapper.toPublic(environmentInfo));
}
async toggleEnvironmentOn(
req: IAuthRequest<FeatureStrategyParams, any, any, any>,
res: Response,
res: Response<void>,
): Promise<void> {
const { featureName, environment, projectId } = req.params;
await this.featureService.updateEnabled(
@ -321,7 +476,7 @@ export default class ProjectFeaturesController extends Controller {
async toggleEnvironmentOff(
req: IAuthRequest<FeatureStrategyParams, any, any, any>,
res: Response,
res: Response<void>,
): Promise<void> {
const { featureName, environment, projectId } = req.params;
await this.featureService.updateEnabled(
@ -335,22 +490,22 @@ export default class ProjectFeaturesController extends Controller {
}
async addStrategy(
req: IAuthRequest<FeatureStrategyParams, any, IStrategyConfig>,
req: IAuthRequest<FeatureStrategyParams, any, CreateStrategySchema>,
res: Response<StrategySchema>,
): Promise<void> {
const { projectId, featureName, environment } = req.params;
const userName = extractUsername(req);
const featureStrategy = await this.featureService.createStrategy(
req.body,
const strategy = await this.featureService.createStrategy(
this.strategyMapper.mapInput(req.body),
{ environment, projectId, featureName },
userName,
);
res.status(200).json(featureStrategy);
res.status(200).json(this.strategyMapper.toPublic(strategy));
}
async getStrategies(
req: Request<FeatureStrategyParams, any, any, any>,
res: Response,
res: Response<StrategySchema[]>,
): Promise<void> {
const { projectId, featureName, environment } = req.params;
const featureStrategies =
@ -359,11 +514,13 @@ export default class ProjectFeaturesController extends Controller {
featureName,
environment,
);
res.status(200).json(featureStrategies);
res.status(200).json(
featureStrategies.map(this.strategyMapper.toPublic),
);
}
async updateStrategy(
req: IAuthRequest<StrategyIdParams, any, CreateStrategySchema>,
req: IAuthRequest<StrategyIdParams, any, UpdateStrategySchema>,
res: Response<StrategySchema>,
): Promise<void> {
const { strategyId, environment, projectId, featureName } = req.params;
@ -374,12 +531,12 @@ export default class ProjectFeaturesController extends Controller {
{ environment, projectId, featureName },
userName,
);
res.status(200).json(updatedStrategy);
res.status(200).json(this.strategyMapper.fromPublic(updatedStrategy));
}
async patchStrategy(
req: IAuthRequest<StrategyIdParams, any, Operation[], any>,
res: Response,
res: Response<StrategySchema>,
): Promise<void> {
const { strategyId, projectId, environment, featureName } = req.params;
const userName = extractUsername(req);
@ -392,23 +549,23 @@ export default class ProjectFeaturesController extends Controller {
{ environment, projectId, featureName },
userName,
);
res.status(200).json(updatedStrategy);
res.status(200).json(this.strategyMapper.toPublic(updatedStrategy));
}
async getStrategy(
req: IAuthRequest<StrategyIdParams, any, any, any>,
res: Response,
res: Response<StrategySchema>,
): Promise<void> {
this.logger.info('Getting strategy');
const { strategyId } = req.params;
this.logger.info(strategyId);
const strategy = await this.featureService.getStrategy(strategyId);
res.status(200).json(strategy);
res.status(200).json(this.strategyMapper.toPublic(strategy));
}
async deleteStrategy(
req: IAuthRequest<StrategyIdParams, any, any, any>,
res: Response,
res: Response<void>,
): Promise<void> {
this.logger.info('Deleting strategy');
const { environment, projectId, featureName } = req.params;
@ -430,7 +587,7 @@ export default class ProjectFeaturesController extends Controller {
{ name: string; value: string | number },
any
>,
res: Response,
res: Response<StrategySchema>,
): Promise<void> {
const { strategyId, environment, projectId, featureName } = req.params;
const userName = extractUsername(req);
@ -444,12 +601,12 @@ export default class ProjectFeaturesController extends Controller {
{ environment, projectId, featureName },
userName,
);
res.status(200).json(updatedStrategy);
res.status(200).json(this.strategyMapper.toPublic(updatedStrategy));
}
async getStrategyParameters(
req: Request<StrategyIdParams, any, any, any>,
res: Response,
res: Response<ParametersSchema>,
): Promise<void> {
this.logger.info('Getting strategy parameters');
const { strategyId } = req.params;

View File

@ -21,7 +21,7 @@ interface IRequestHandler<
interface IRouteOptions {
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
path: string;
permission?: string;
permission: string;
middleware?: RequestHandler[];
handler: IRequestHandler;
acceptAnyContentType?: boolean;

View File

@ -1,5 +1,5 @@
import { IUnleashConfig } from '../types/option';
import { IUnleashStores } from '../types/stores';
import { IUnleashStores } from '../types';
import { Logger } from '../logger';
import BadDataError from '../error/bad-data-error';
import NameExistsError from '../error/name-exists-error';
@ -424,7 +424,7 @@ class FeatureToggleService {
* }
* @param id - strategy id
* @param context - Which context does this strategy live in (projectId, featureName, environment)
* @param environment - Which environment does this strategy belong to
* @param createdBy - Which user does this strategy belong to
*/
async deleteStrategy(
id: string,
@ -529,7 +529,6 @@ class FeatureToggleService {
* Used to retrieve metadata of all feature toggles defined in Unleash.
* @param query - Allow you to limit search based on criteria such as project, tags, namePrefix. See @IFeatureToggleQuery
* @param archived - Return archived or active toggles
* @param includeStrategyId - Include id for strategies
* @returns
*/
async getFeatureToggles(
@ -1054,7 +1053,7 @@ class FeatureToggleService {
createdBy,
tags,
oldVariants,
newVariants: featureToggle.variants,
newVariants: featureToggle.variants as IVariant[],
}),
);
return featureToggle;

View File

@ -6,21 +6,21 @@ import { nameType } from '../routes/util';
import { projectSchema } from './project-schema';
import NotFoundError from '../error/notfound-error';
import {
ProjectUserAddedEvent,
ProjectUserRemovedEvent,
ProjectUserUpdateRoleEvent,
PROJECT_CREATED,
PROJECT_DELETED,
PROJECT_UPDATED,
ProjectUserAddedEvent,
ProjectUserRemovedEvent,
ProjectUserUpdateRoleEvent,
} from '../types/events';
import { IUnleashStores } from '../types/stores';
import { IUnleashStores } from '../types';
import { IUnleashConfig } from '../types/option';
import {
FeatureToggle,
IProject,
IProjectOverview,
IProjectWithCount,
IUserWithRole,
FeatureToggle,
RoleName,
} from '../types/model';
import { IEnvironmentStore } from '../types/stores/environment-store';

View File

@ -69,7 +69,7 @@ export class SegmentService {
async create(data: unknown, user: User): Promise<void> {
const input = await segmentSchema.validateAsync(data);
this.validateSegmentValuesLimit(input);
SegmentService.validateSegmentValuesLimit(input);
await this.validateName(input.name);
const segment = await this.segmentStore.create(input, user);
@ -83,7 +83,7 @@ export class SegmentService {
async update(id: number, data: unknown, user: User): Promise<void> {
const input = await segmentSchema.validateAsync(data);
this.validateSegmentValuesLimit(input);
SegmentService.validateSegmentValuesLimit(input);
const preData = await this.segmentStore.get(id);
if (preData.name !== input.name) {
@ -147,7 +147,9 @@ export class SegmentService {
}
}
private validateSegmentValuesLimit(segment: Omit<ISegment, 'id'>): void {
private static validateSegmentValuesLimit(
segment: Omit<ISegment, 'id'>,
): void {
const limit = SEGMENT_VALUES_LIMIT;
const valuesCount = segment.constraints

View File

@ -0,0 +1,4 @@
// Create a string with allowed values from a values array. ['A', 'B'] => 'A' | 'B'
export type AllowedStrings<T extends ReadonlyArray<unknown>> =
// eslint-disable-next-line @typescript-eslint/no-shadow
T extends ReadonlyArray<infer AllowedStrings> ? AllowedStrings : never;

View File

@ -2,10 +2,14 @@ import { ITagType } from './stores/tag-type-store';
import { LogProvider } from '../logger';
import { IRole } from './stores/access-store';
import { IUser } from './user';
import { ALL_OPERATORS } from '../util/constants';
import { AllowedStrings } from './allowed-strings';
export type Operator = AllowedStrings<typeof ALL_OPERATORS>;
export interface IConstraint {
contextName: string;
operator: string;
operator: Operator;
values?: string[];
value?: string;
inverted?: boolean;

View File

@ -40,7 +40,7 @@ export const ALL_OPERATORS = [
SEMVER_EQ,
SEMVER_GT,
SEMVER_LT,
];
] as const;
export const STRING_OPERATORS = [
STR_ENDS_WITH,

View File

@ -1,16 +0,0 @@
import { serializeDates } from './serialize-dates';
test('serializeDates', () => {
const obj = {
a: 1,
b: '2',
c: new Date(),
d: { e: new Date() },
};
expect(serializeDates({})).toEqual({});
expect(serializeDates(obj).a).toEqual(1);
expect(serializeDates(obj).b).toEqual('2');
expect(typeof serializeDates(obj).c).toEqual('string');
expect(typeof serializeDates(obj).d.e).toEqual('object');
});

View File

@ -1,22 +0,0 @@
type SerializedDates<T> = {
[P in keyof T]: T[P] extends Date ? string : T[P];
};
// Disallow array arguments for serializeDates.
// Use `array.map(serializeDates)` instead.
type NotArray<T> = Exclude<T, unknown[]>;
// Serialize top-level date values to strings.
export const serializeDates = <T extends object>(
obj: NotArray<T>,
): SerializedDates<T> => {
const entries = Object.entries(obj).map(([k, v]) => {
if (v instanceof Date) {
return [k, v.toJSON()];
} else {
return [k, v];
}
});
return Object.fromEntries(entries);
};

View File

@ -1,5 +1,4 @@
import faker from 'faker';
import { FeatureToggleDTO, IStrategyConfig, IVariant } from 'lib/types/model';
import dbInit, { ITestDb } from '../../helpers/database-init';
import {
IUnleashTest,
@ -8,6 +7,9 @@ import {
} from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
import { StrategySchema } from '../../../../lib/openapi/spec/strategy-schema';
import { FeatureSchema } from '../../../../lib/openapi/spec/feature-schema';
import { VariantSchema } from '../../../../lib/openapi/spec/variant-schema';
let app: IUnleashTest;
let db: ITestDb;
@ -23,8 +25,8 @@ beforeAll(async () => {
app = await setupApp(db.stores);
const createToggle = async (
toggle: FeatureToggleDTO,
strategy: Omit<IStrategyConfig, 'id'> = defaultStrategy,
toggle: FeatureSchema,
strategy: Omit<StrategySchema, 'id'> = defaultStrategy,
projectId: string = 'default',
username: string = 'test',
) => {
@ -41,7 +43,7 @@ beforeAll(async () => {
};
const createVariants = async (
featureName: string,
variants: IVariant[],
variants: VariantSchema[],
projectId: string = 'default',
username: string = 'test',
) => {
@ -56,12 +58,14 @@ beforeAll(async () => {
await createToggle({
name: 'featureX',
description: 'the #1 feature',
project: 'some-project',
});
await createToggle(
{
name: 'featureY',
description: 'soon to be the #1 feature',
project: 'some-project',
},
{
name: 'baz',
@ -76,6 +80,7 @@ beforeAll(async () => {
{
name: 'featureZ',
description: 'terrible feature',
project: 'some-project',
},
{
name: 'baz',
@ -90,6 +95,7 @@ beforeAll(async () => {
{
name: 'featureArchivedX',
description: 'the #1 feature',
project: 'some-project',
},
{
name: 'default',
@ -107,6 +113,7 @@ beforeAll(async () => {
{
name: 'featureArchivedY',
description: 'soon to be the #1 feature',
project: 'some-project',
},
{
name: 'baz',
@ -126,13 +133,14 @@ beforeAll(async () => {
{
name: 'featureArchivedZ',
description: 'terrible feature',
project: 'some-project',
},
{
name: 'baz',
constraints: [],
parameters: {
foo: 'rab',
},
constraints: [],
},
);
@ -144,6 +152,7 @@ beforeAll(async () => {
await createToggle({
name: 'feature.with.variants',
description: 'A feature toggle with variants',
project: 'some-project',
});
await createVariants('feature.with.variants', [
{

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
import { IStrategyConfig } from '../../../lib/types/model';
import { createTestConfig } from '../../config/test-config';
import dbInit from '../helpers/database-init';
import { DEFAULT_ENV } from '../../../lib/util/constants';
import { StrategySchema } from '../../../lib/openapi/spec/strategy-schema';
let stores;
let db;
@ -25,7 +25,7 @@ afterAll(async () => {
test('Should create feature toggle strategy configuration', async () => {
const projectId = 'default';
const username = 'feature-toggle';
const config: Omit<IStrategyConfig, 'id'> = {
const config: Omit<StrategySchema, 'id'> = {
name: 'default',
constraints: [],
parameters: {},
@ -53,7 +53,7 @@ test('Should be able to update existing strategy configuration', async () => {
const projectId = 'default';
const username = 'existing-strategy';
const featureName = 'update-existing-strategy';
const config: Omit<IStrategyConfig, 'id'> = {
const config: Omit<StrategySchema, 'id'> = {
name: 'default',
constraints: [],
parameters: {},
@ -88,7 +88,7 @@ test('Should be able to get strategy by id', async () => {
const projectId = 'default';
const userName = 'strategy';
const config: Omit<IStrategyConfig, 'id'> = {
const config: Omit<StrategySchema, 'id'> = {
name: 'default',
constraints: [],
parameters: {},

View File

@ -1,11 +1,11 @@
import { randomUUID } from 'crypto';
import {
FeatureToggle,
FeatureToggleWithEnvironment,
IFeatureOverview,
IFeatureStrategy,
IFeatureToggleClient,
IFeatureToggleQuery,
IFeatureStrategy,
FeatureToggle,
} from '../../lib/types/model';
import NotFoundError from '../../lib/error/notfound-error';
import { IFeatureStrategiesStore } from '../../lib/types/stores/feature-strategies-store';

View File

@ -2,12 +2,8 @@ import {
IFeatureToggleQuery,
IFeatureToggleStore,
} from '../../lib/types/stores/feature-toggle-store';
import {
FeatureToggle,
FeatureToggleDTO,
IVariant,
} from '../../lib/types/model';
import NotFoundError from '../../lib/error/notfound-error';
import { FeatureToggle, FeatureToggleDTO, IVariant } from 'lib/types/model';
export default class FakeFeatureToggleStore implements IFeatureToggleStore {
features: FeatureToggle[] = [];
@ -49,10 +45,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
};
}
async create(
project: string,
data: FeatureToggleDTO,
): Promise<FeatureToggle> {
async create(project: string, data: FeatureToggle): Promise<FeatureToggle> {
const inserted: FeatureToggle = { ...data, project };
this.features.push(inserted);
return inserted;
@ -130,7 +123,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
async getVariants(featureName: string): Promise<IVariant[]> {
const feature = await this.get(featureName);
return feature.variants;
return feature.variants as IVariant[];
}
async saveVariants(