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:
parent
7d1a5c2012
commit
1a27bffe4d
1
.gitignore
vendored
1
.gitignore
vendored
@ -54,3 +54,4 @@ package-lock.json
|
||||
/website/i18n/*
|
||||
.env
|
||||
|
||||
report.json
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
29
src/lib/openapi/mappers/environment-info.mapper.ts
Normal file
29
src/lib/openapi/mappers/environment-info.mapper.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
29
src/lib/openapi/mappers/feature-strategy.mapper.ts
Normal file
29
src/lib/openapi/mappers/feature-strategy.mapper.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
3
src/lib/openapi/mappers/index.ts
Normal file
3
src/lib/openapi/mappers/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './environment-info.mapper';
|
||||
export * from './feature-strategy.mapper';
|
||||
export * from './strategy.mapper';
|
7
src/lib/openapi/mappers/mapper.ts
Normal file
7
src/lib/openapi/mappers/mapper.ts
Normal 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;
|
||||
}
|
33
src/lib/openapi/mappers/strategy.mapper.ts
Normal file
33
src/lib/openapi/mappers/strategy.mapper.ts
Normal 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 || [],
|
||||
};
|
||||
}
|
||||
}
|
12
src/lib/openapi/spec/clone-feature-request.ts
Normal file
12
src/lib/openapi/spec/clone-feature-request.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
19
src/lib/openapi/spec/clone-feature-schema.ts
Normal file
19
src/lib/openapi/spec/clone-feature-schema.ts
Normal 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);
|
@ -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>;
|
||||
|
@ -17,6 +17,7 @@ const schema = {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
'components/schemas': {},
|
||||
} as const;
|
||||
|
||||
export type CreateFeatureSchema = CreateSchemaType<typeof schema>;
|
||||
|
@ -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;
|
||||
|
||||
|
12
src/lib/openapi/spec/create-tag-request.ts
Normal file
12
src/lib/openapi/spec/create-tag-request.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
11
src/lib/openapi/spec/empty-response-schema.ts
Normal file
11
src/lib/openapi/spec/empty-response-schema.ts
Normal 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);
|
12
src/lib/openapi/spec/empty-response.ts
Normal file
12
src/lib/openapi/spec/empty-response.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
12
src/lib/openapi/spec/feature-environment-info-response.ts
Normal file
12
src/lib/openapi/spec/feature-environment-info-response.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
35
src/lib/openapi/spec/feature-environment-info-schema.ts
Normal file
35
src/lib/openapi/spec/feature-environment-info-schema.ts
Normal 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);
|
@ -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>;
|
||||
|
58
src/lib/openapi/spec/feature-strategy-schema.ts
Normal file
58
src/lib/openapi/spec/feature-strategy-schema.ts
Normal 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);
|
@ -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>;
|
||||
|
@ -15,6 +15,7 @@ const schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
'components/schemas': {},
|
||||
} as const;
|
||||
|
||||
export type OverrideSchema = CreateSchemaType<typeof schema>;
|
||||
|
@ -6,6 +6,7 @@ const schema = {
|
||||
type: 'string',
|
||||
maxLength: 100,
|
||||
},
|
||||
'components/schemas': {},
|
||||
} as const;
|
||||
|
||||
export type ParametersSchema = CreateSchemaType<typeof schema>;
|
||||
|
24
src/lib/openapi/spec/patch-operation-schema.ts
Normal file
24
src/lib/openapi/spec/patch-operation-schema.ts
Normal 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);
|
15
src/lib/openapi/spec/patch-request.ts
Normal file
15
src/lib/openapi/spec/patch-request.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
15
src/lib/openapi/spec/strategies-response.ts
Normal file
15
src/lib/openapi/spec/strategies-response.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -5,7 +5,7 @@ export const strategyResponse: OpenAPIV3.ResponseObject = {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/strategySchema',
|
||||
$ref: '#/components/schemas/featureStrategySchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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);
|
||||
|
12
src/lib/openapi/spec/tag-response.ts
Normal file
12
src/lib/openapi/spec/tag-response.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
20
src/lib/openapi/spec/tag-schema.ts
Normal file
20
src/lib/openapi/spec/tag-schema.ts
Normal 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);
|
26
src/lib/openapi/spec/tags-response-schema.ts
Normal file
26
src/lib/openapi/spec/tags-response-schema.ts
Normal 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);
|
12
src/lib/openapi/spec/tags-response.ts
Normal file
12
src/lib/openapi/spec/tags-response.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
12
src/lib/openapi/spec/update-feature-request.ts
Normal file
12
src/lib/openapi/spec/update-feature-request.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
12
src/lib/openapi/spec/update-strategy-request.ts
Normal file
12
src/lib/openapi/spec/update-strategy-request.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
11
src/lib/openapi/spec/update-strategy-schema.ts
Normal file
11
src/lib/openapi/spec/update-strategy-schema.ts
Normal 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);
|
42
src/lib/openapi/spec/updateFeatureSchema.ts
Normal file
42
src/lib/openapi/spec/updateFeatureSchema.ts
Normal 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);
|
@ -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>;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
|
4
src/lib/types/allowed-strings.ts
Normal file
4
src/lib/types/allowed-strings.ts
Normal 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;
|
@ -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;
|
||||
|
@ -40,7 +40,7 @@ export const ALL_OPERATORS = [
|
||||
SEMVER_EQ,
|
||||
SEMVER_GT,
|
||||
SEMVER_LT,
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const STRING_OPERATORS = [
|
||||
STR_ENDS_WITH,
|
||||
|
@ -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');
|
||||
});
|
@ -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);
|
||||
};
|
@ -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
@ -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: {},
|
||||
|
@ -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';
|
||||
|
13
src/test/fixtures/fake-feature-toggle-store.ts
vendored
13
src/test/fixtures/fake-feature-toggle-store.ts
vendored
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user