From 1a27bffe4d495770f662e93fdcadd9f27714aed8 Mon Sep 17 00:00:00 2001 From: andreas-unleash <104830839+andreas-unleash@users.noreply.github.com> Date: Wed, 18 May 2022 16:17:09 +0300 Subject: [PATCH] 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 --- .gitignore | 1 + src/lib/db/feature-strategy-store.ts | 14 +- src/lib/db/feature-toggle-client-store.ts | 12 +- src/lib/openapi/index.ts | 28 +- .../mappers/environment-info.mapper.ts | 29 + .../mappers/feature-strategy.mapper.ts | 29 + src/lib/openapi/mappers/index.ts | 3 + src/lib/openapi/mappers/mapper.ts | 7 + src/lib/openapi/mappers/strategy.mapper.ts | 33 + src/lib/openapi/spec/clone-feature-request.ts | 12 + src/lib/openapi/spec/clone-feature-schema.ts | 19 + src/lib/openapi/spec/constraint-schema.ts | 3 + src/lib/openapi/spec/create-feature-schema.ts | 1 + .../openapi/spec/create-strategy-schema.ts | 10 +- src/lib/openapi/spec/create-tag-request.ts | 12 + src/lib/openapi/spec/empty-response-schema.ts | 11 + src/lib/openapi/spec/empty-response.ts | 12 + .../spec/feature-environment-info-response.ts | 12 + .../spec/feature-environment-info-schema.ts | 35 + src/lib/openapi/spec/feature-schema.ts | 21 +- .../openapi/spec/feature-strategy-schema.ts | 58 + src/lib/openapi/spec/features-schema.ts | 5 +- src/lib/openapi/spec/override-schema.ts | 1 + src/lib/openapi/spec/parameters-schema.ts | 1 + .../openapi/spec/patch-operation-schema.ts | 24 + src/lib/openapi/spec/patch-request.ts | 15 + src/lib/openapi/spec/strategies-response.ts | 15 + src/lib/openapi/spec/strategy-response.ts | 2 +- src/lib/openapi/spec/strategy-schema.ts | 19 +- src/lib/openapi/spec/tag-response.ts | 12 + src/lib/openapi/spec/tag-schema.ts | 20 + src/lib/openapi/spec/tags-response-schema.ts | 26 + src/lib/openapi/spec/tags-response.ts | 12 + .../openapi/spec/update-feature-request.ts | 12 + .../openapi/spec/update-strategy-request.ts | 12 + .../openapi/spec/update-strategy-schema.ts | 11 + src/lib/openapi/spec/updateFeatureSchema.ts | 42 + src/lib/openapi/spec/variant-schema.ts | 14 +- src/lib/openapi/types.ts | 28 +- src/lib/routes/admin-api/archive.ts | 11 +- src/lib/routes/admin-api/feature.ts | 109 +- src/lib/routes/admin-api/project/features.ts | 287 +++- src/lib/routes/controller.ts | 2 +- src/lib/services/feature-toggle-service.ts | 7 +- src/lib/services/project-service.ts | 10 +- src/lib/services/segment-service.ts | 8 +- src/lib/types/allowed-strings.ts | 4 + src/lib/types/model.ts | 6 +- src/lib/util/constants.ts | 2 +- src/lib/util/serialize-dates.test.ts | 16 - src/lib/util/serialize-dates.ts | 22 - src/test/e2e/api/admin/feature.e2e.test.ts | 19 +- .../__snapshots__/openapi.e2e.test.ts.snap | 1294 ++++++++++++----- .../feature-toggle-service-v2.e2e.test.ts | 8 +- .../fixtures/fake-feature-strategies-store.ts | 4 +- .../fixtures/fake-feature-toggle-store.ts | 13 +- 56 files changed, 1916 insertions(+), 539 deletions(-) create mode 100644 src/lib/openapi/mappers/environment-info.mapper.ts create mode 100644 src/lib/openapi/mappers/feature-strategy.mapper.ts create mode 100644 src/lib/openapi/mappers/index.ts create mode 100644 src/lib/openapi/mappers/mapper.ts create mode 100644 src/lib/openapi/mappers/strategy.mapper.ts create mode 100644 src/lib/openapi/spec/clone-feature-request.ts create mode 100644 src/lib/openapi/spec/clone-feature-schema.ts create mode 100644 src/lib/openapi/spec/create-tag-request.ts create mode 100644 src/lib/openapi/spec/empty-response-schema.ts create mode 100644 src/lib/openapi/spec/empty-response.ts create mode 100644 src/lib/openapi/spec/feature-environment-info-response.ts create mode 100644 src/lib/openapi/spec/feature-environment-info-schema.ts create mode 100644 src/lib/openapi/spec/feature-strategy-schema.ts create mode 100644 src/lib/openapi/spec/patch-operation-schema.ts create mode 100644 src/lib/openapi/spec/patch-request.ts create mode 100644 src/lib/openapi/spec/strategies-response.ts create mode 100644 src/lib/openapi/spec/tag-response.ts create mode 100644 src/lib/openapi/spec/tag-schema.ts create mode 100644 src/lib/openapi/spec/tags-response-schema.ts create mode 100644 src/lib/openapi/spec/tags-response.ts create mode 100644 src/lib/openapi/spec/update-feature-request.ts create mode 100644 src/lib/openapi/spec/update-strategy-request.ts create mode 100644 src/lib/openapi/spec/update-strategy-schema.ts create mode 100644 src/lib/openapi/spec/updateFeatureSchema.ts create mode 100644 src/lib/types/allowed-strings.ts delete mode 100644 src/lib/util/serialize-dates.test.ts delete mode 100644 src/lib/util/serialize-dates.ts diff --git a/.gitignore b/.gitignore index a5e6828db1..a52b4c61f1 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ package-lock.json /website/i18n/* .env +report.json diff --git a/src/lib/db/feature-strategy-store.ts b/src/lib/db/feature-strategy-store.ts index 8a3c77335b..9b3f64b0eb 100644 --- a/src/lib/db/feature-strategy-store.ts +++ b/src/lib/db/feature-strategy-store.ts @@ -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 { diff --git a/src/lib/db/feature-toggle-client-store.ts b/src/lib/db/feature-toggle-client-store.ts index 6cbac56119..09957dba7a 100644 --- a/src/lib/db/feature-toggle-client-store.ts +++ b/src/lib/db/feature-toggle-client-store.ts @@ -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): IStrategyConfig { + private static rowToStrategy(row: Record): 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; diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index d5d98ca5d4..9352d03f5c 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -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, }, }, }; diff --git a/src/lib/openapi/mappers/environment-info.mapper.ts b/src/lib/openapi/mappers/environment-info.mapper.ts new file mode 100644 index 0000000000..fa5d850d43 --- /dev/null +++ b/src/lib/openapi/mappers/environment-info.mapper.ts @@ -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 + > +{ + 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), + }; + } +} diff --git a/src/lib/openapi/mappers/feature-strategy.mapper.ts b/src/lib/openapi/mappers/feature-strategy.mapper.ts new file mode 100644 index 0000000000..f0be1ef3ff --- /dev/null +++ b/src/lib/openapi/mappers/feature-strategy.mapper.ts @@ -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, + }; + } +} diff --git a/src/lib/openapi/mappers/index.ts b/src/lib/openapi/mappers/index.ts new file mode 100644 index 0000000000..984f8c1ccd --- /dev/null +++ b/src/lib/openapi/mappers/index.ts @@ -0,0 +1,3 @@ +export * from './environment-info.mapper'; +export * from './feature-strategy.mapper'; +export * from './strategy.mapper'; diff --git a/src/lib/openapi/mappers/mapper.ts b/src/lib/openapi/mappers/mapper.ts new file mode 100644 index 0000000000..80c88d4166 --- /dev/null +++ b/src/lib/openapi/mappers/mapper.ts @@ -0,0 +1,7 @@ +// Convert between public schema types and internal data types. +// Avoids coupling public schemas to internal implementation details. +export interface SchemaMapper> { + fromPublic(input: SCHEMA): INTERNAL; + toPublic(input: INTERNAL): SCHEMA; + mapInput?(input: INPUT): INTERNAL; +} diff --git a/src/lib/openapi/mappers/strategy.mapper.ts b/src/lib/openapi/mappers/strategy.mapper.ts new file mode 100644 index 0000000000..118a8655d7 --- /dev/null +++ b/src/lib/openapi/mappers/strategy.mapper.ts @@ -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 || [], + }; + } +} diff --git a/src/lib/openapi/spec/clone-feature-request.ts b/src/lib/openapi/spec/clone-feature-request.ts new file mode 100644 index 0000000000..531677bd97 --- /dev/null +++ b/src/lib/openapi/spec/clone-feature-request.ts @@ -0,0 +1,12 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const cloneFeatureRequest: OpenAPIV3.RequestBodyObject = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/cloneFeatureSchema', + }, + }, + }, +}; diff --git a/src/lib/openapi/spec/clone-feature-schema.ts b/src/lib/openapi/spec/clone-feature-schema.ts new file mode 100644 index 0000000000..c8901031a8 --- /dev/null +++ b/src/lib/openapi/spec/clone-feature-schema.ts @@ -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; + +export const cloneFeatureSchema = createSchemaObject(schema); diff --git a/src/lib/openapi/spec/constraint-schema.ts b/src/lib/openapi/spec/constraint-schema.ts index c62e9d854d..3b003b4ad4 100644 --- a/src/lib/openapi/spec/constraint-schema.ts +++ b/src/lib/openapi/spec/constraint-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/create-feature-schema.ts b/src/lib/openapi/spec/create-feature-schema.ts index ce0a28e720..84daa9ebbf 100644 --- a/src/lib/openapi/spec/create-feature-schema.ts +++ b/src/lib/openapi/spec/create-feature-schema.ts @@ -17,6 +17,7 @@ const schema = { type: 'boolean', }, }, + 'components/schemas': {}, } as const; export type CreateFeatureSchema = CreateSchemaType; diff --git a/src/lib/openapi/spec/create-strategy-schema.ts b/src/lib/openapi/spec/create-strategy-schema.ts index 821b731ce3..7ad73ed304 100644 --- a/src/lib/openapi/spec/create-strategy-schema.ts +++ b/src/lib/openapi/spec/create-strategy-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/create-tag-request.ts b/src/lib/openapi/spec/create-tag-request.ts new file mode 100644 index 0000000000..37ff3c17f4 --- /dev/null +++ b/src/lib/openapi/spec/create-tag-request.ts @@ -0,0 +1,12 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const createTagRequest: OpenAPIV3.RequestBodyObject = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/tagSchema', + }, + }, + }, +}; diff --git a/src/lib/openapi/spec/empty-response-schema.ts b/src/lib/openapi/spec/empty-response-schema.ts new file mode 100644 index 0000000000..5df5784b02 --- /dev/null +++ b/src/lib/openapi/spec/empty-response-schema.ts @@ -0,0 +1,11 @@ +import { createSchemaObject, CreateSchemaType } from '../types'; + +const schema = { + type: 'object', + description: 'OK', + 'components/schemas': {}, +} as const; + +export type EmptyResponseSchema = CreateSchemaType; + +export const emptyResponseSchema = createSchemaObject(schema); diff --git a/src/lib/openapi/spec/empty-response.ts b/src/lib/openapi/spec/empty-response.ts new file mode 100644 index 0000000000..7efd6cd6db --- /dev/null +++ b/src/lib/openapi/spec/empty-response.ts @@ -0,0 +1,12 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const emptyResponse: OpenAPIV3.ResponseObject = { + description: 'emptyResponse', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/emptyResponseSchema', + }, + }, + }, +}; diff --git a/src/lib/openapi/spec/feature-environment-info-response.ts b/src/lib/openapi/spec/feature-environment-info-response.ts new file mode 100644 index 0000000000..dac7cabb1b --- /dev/null +++ b/src/lib/openapi/spec/feature-environment-info-response.ts @@ -0,0 +1,12 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const featureEnvironmentInfoResponse: OpenAPIV3.ResponseObject = { + description: 'featureEnvironmentInfoResponse', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/featureEnvironmentInfoSchema', + }, + }, + }, +}; diff --git a/src/lib/openapi/spec/feature-environment-info-schema.ts b/src/lib/openapi/spec/feature-environment-info-schema.ts new file mode 100644 index 0000000000..02238be3c6 --- /dev/null +++ b/src/lib/openapi/spec/feature-environment-info-schema.ts @@ -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; + +export const featureEnvironmentInfoSchema = createSchemaObject(schema); diff --git a/src/lib/openapi/spec/feature-schema.ts b/src/lib/openapi/spec/feature-schema.ts index 148d4238ed..f29d75a740 100644 --- a/src/lib/openapi/spec/feature-schema.ts +++ b/src/lib/openapi/spec/feature-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/feature-strategy-schema.ts b/src/lib/openapi/spec/feature-strategy-schema.ts new file mode 100644 index 0000000000..85fe1574f3 --- /dev/null +++ b/src/lib/openapi/spec/feature-strategy-schema.ts @@ -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; + +export const featureStrategySchema = createSchemaObject(schema); diff --git a/src/lib/openapi/spec/features-schema.ts b/src/lib/openapi/spec/features-schema.ts index f9164996bd..c468703609 100644 --- a/src/lib/openapi/spec/features-schema.ts +++ b/src/lib/openapi/spec/features-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/override-schema.ts b/src/lib/openapi/spec/override-schema.ts index 9e02e7a880..6d103e4a06 100644 --- a/src/lib/openapi/spec/override-schema.ts +++ b/src/lib/openapi/spec/override-schema.ts @@ -15,6 +15,7 @@ const schema = { }, }, }, + 'components/schemas': {}, } as const; export type OverrideSchema = CreateSchemaType; diff --git a/src/lib/openapi/spec/parameters-schema.ts b/src/lib/openapi/spec/parameters-schema.ts index f72eda1353..a6c7160159 100644 --- a/src/lib/openapi/spec/parameters-schema.ts +++ b/src/lib/openapi/spec/parameters-schema.ts @@ -6,6 +6,7 @@ const schema = { type: 'string', maxLength: 100, }, + 'components/schemas': {}, } as const; export type ParametersSchema = CreateSchemaType; diff --git a/src/lib/openapi/spec/patch-operation-schema.ts b/src/lib/openapi/spec/patch-operation-schema.ts new file mode 100644 index 0000000000..15de7598b4 --- /dev/null +++ b/src/lib/openapi/spec/patch-operation-schema.ts @@ -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; + +export const patchOperationSchema = createSchemaObject(schema); diff --git a/src/lib/openapi/spec/patch-request.ts b/src/lib/openapi/spec/patch-request.ts new file mode 100644 index 0000000000..654c9176d7 --- /dev/null +++ b/src/lib/openapi/spec/patch-request.ts @@ -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', + }, + }, + }, + }, +}; diff --git a/src/lib/openapi/spec/strategies-response.ts b/src/lib/openapi/spec/strategies-response.ts new file mode 100644 index 0000000000..d4b69fc47c --- /dev/null +++ b/src/lib/openapi/spec/strategies-response.ts @@ -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', + }, + }, + }, + }, +}; diff --git a/src/lib/openapi/spec/strategy-response.ts b/src/lib/openapi/spec/strategy-response.ts index b00a572752..23a8c49399 100644 --- a/src/lib/openapi/spec/strategy-response.ts +++ b/src/lib/openapi/spec/strategy-response.ts @@ -5,7 +5,7 @@ export const strategyResponse: OpenAPIV3.ResponseObject = { content: { 'application/json': { schema: { - $ref: '#/components/schemas/strategySchema', + $ref: '#/components/schemas/featureStrategySchema', }, }, }, diff --git a/src/lib/openapi/spec/strategy-schema.ts b/src/lib/openapi/spec/strategy-schema.ts index aa4ce190ce..7f22432c99 100644 --- a/src/lib/openapi/spec/strategy-schema.ts +++ b/src/lib/openapi/spec/strategy-schema.ts @@ -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; +export type StrategySchema = CreateSchemaType; -export const strategySchema = createSchemaObject(schema); +export const strategySchema = createSchemaObject(strategySchemaDefinition); diff --git a/src/lib/openapi/spec/tag-response.ts b/src/lib/openapi/spec/tag-response.ts new file mode 100644 index 0000000000..6c4654d90d --- /dev/null +++ b/src/lib/openapi/spec/tag-response.ts @@ -0,0 +1,12 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const tagResponse: OpenAPIV3.ResponseObject = { + description: 'tagResponse', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/tagSchema', + }, + }, + }, +}; diff --git a/src/lib/openapi/spec/tag-schema.ts b/src/lib/openapi/spec/tag-schema.ts new file mode 100644 index 0000000000..2f84dc6db8 --- /dev/null +++ b/src/lib/openapi/spec/tag-schema.ts @@ -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; + +export const tagSchema = createSchemaObject(schema); diff --git a/src/lib/openapi/spec/tags-response-schema.ts b/src/lib/openapi/spec/tags-response-schema.ts new file mode 100644 index 0000000000..284d969193 --- /dev/null +++ b/src/lib/openapi/spec/tags-response-schema.ts @@ -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; + +export const tagsResponseSchema = createSchemaObject(schema); diff --git a/src/lib/openapi/spec/tags-response.ts b/src/lib/openapi/spec/tags-response.ts new file mode 100644 index 0000000000..61191ff76f --- /dev/null +++ b/src/lib/openapi/spec/tags-response.ts @@ -0,0 +1,12 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const tagsResponse: OpenAPIV3.ResponseObject = { + description: 'tagsResponse', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/tagsResponseSchema', + }, + }, + }, +}; diff --git a/src/lib/openapi/spec/update-feature-request.ts b/src/lib/openapi/spec/update-feature-request.ts new file mode 100644 index 0000000000..2f153e75f7 --- /dev/null +++ b/src/lib/openapi/spec/update-feature-request.ts @@ -0,0 +1,12 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const updateFeatureRequest: OpenAPIV3.RequestBodyObject = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/updateFeatureSchema', + }, + }, + }, +}; diff --git a/src/lib/openapi/spec/update-strategy-request.ts b/src/lib/openapi/spec/update-strategy-request.ts new file mode 100644 index 0000000000..034551dd7b --- /dev/null +++ b/src/lib/openapi/spec/update-strategy-request.ts @@ -0,0 +1,12 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const updateStrategyRequest: OpenAPIV3.RequestBodyObject = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/updateStrategySchema', + }, + }, + }, +}; diff --git a/src/lib/openapi/spec/update-strategy-schema.ts b/src/lib/openapi/spec/update-strategy-schema.ts new file mode 100644 index 0000000000..271b9679f3 --- /dev/null +++ b/src/lib/openapi/spec/update-strategy-schema.ts @@ -0,0 +1,11 @@ +import { createSchemaObject, CreateSchemaType } from '../types'; +import { strategySchemaDefinition } from './strategy-schema'; + +const schema = { + ...strategySchemaDefinition, + required: [], +} as const; + +export type UpdateStrategySchema = CreateSchemaType; + +export const updateStrategySchema = createSchemaObject(schema); diff --git a/src/lib/openapi/spec/updateFeatureSchema.ts b/src/lib/openapi/spec/updateFeatureSchema.ts new file mode 100644 index 0000000000..7f06d6626e --- /dev/null +++ b/src/lib/openapi/spec/updateFeatureSchema.ts @@ -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; + +export const updateFeatureSchema = createSchemaObject(schema); diff --git a/src/lib/openapi/spec/variant-schema.ts b/src/lib/openapi/spec/variant-schema.ts index a5366f6b94..7557ab7eb0 100644 --- a/src/lib/openapi/spec/variant-schema.ts +++ b/src/lib/openapi/spec/variant-schema.ts @@ -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; diff --git a/src/lib/openapi/types.ts b/src/lib/openapi/types.ts index 7c0fb6aab9..ff16609130 100644 --- a/src/lib/openapi/types.ts +++ b/src/lib/openapi/types.ts @@ -15,7 +15,31 @@ export interface ClientApiOperation } // Create a type from a const schema object. -export type CreateSchemaType = FromSchema; +export type CreateSchemaType = 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 = (schema: T): DeepMutable => 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> => { + const { 'components/schemas': schemas, ...rest } = schema; + return rest; +}; diff --git a/src/lib/routes/admin-api/archive.ts b/src/lib/routes/admin-api/archive.ts index c32c308612..384626402d 100644 --- a/src/lib/routes/admin-api/archive.ts +++ b/src/lib/routes/admin-api/archive.ts @@ -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, }); } diff --git a/src/lib/routes/admin-api/feature.ts b/src/lib/routes/admin-api/feature.ts index 709a6cb41b..d9f2e73a2e 100644 --- a/src/lib/routes/admin-api/feature.ts +++ b/src/lib/routes/admin-api/feature.ts @@ -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 { 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 { + async listTags( + req: Request<{ featureName: string }, any, any, any>, + res: Response, + ): Promise { const tags = await this.tagService.listTags(req.params.featureName); res.json({ version, tags }); } - async addTag(req: IAuthRequest, res: Response): Promise { + async addTag( + req: IAuthRequest< + { featureName: string }, + Response, + TagSchema, + any + >, + res: Response, + ): Promise { 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 { + async removeTag( + req: IAuthRequest<{ featureName: string; type: string; value: string }>, + res: Response, + ): Promise { 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 { + async validate( + req: Request, + res: Response, + ): Promise { const { name } = req.body; await this.service.validateName(name); diff --git a/src/lib/routes/admin-api/project/features.ts b/src/lib/routes/admin-api/project/features.ts index c706722efc..de523839eb 100644 --- a/src/lib/routes/admin-api/project/features.ts +++ b/src/lib/routes/admin-api/project/features.ts @@ -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, - res: Response, + res: Response, ): Promise { 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, ): Promise { 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, ): Promise { 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, ): Promise { 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, ): Promise { const { featureName } = req.params; const userName = extractUsername(req); @@ -293,7 +448,7 @@ export default class ProjectFeaturesController extends Controller { async getEnvironment( req: Request, - res: Response, + res: Response, ): Promise { 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, - res: Response, + res: Response, ): Promise { const { featureName, environment, projectId } = req.params; await this.featureService.updateEnabled( @@ -321,7 +476,7 @@ export default class ProjectFeaturesController extends Controller { async toggleEnvironmentOff( req: IAuthRequest, - res: Response, + res: Response, ): Promise { const { featureName, environment, projectId } = req.params; await this.featureService.updateEnabled( @@ -335,22 +490,22 @@ export default class ProjectFeaturesController extends Controller { } async addStrategy( - req: IAuthRequest, + req: IAuthRequest, res: Response, ): Promise { 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, - res: Response, + res: Response, ): Promise { 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, + req: IAuthRequest, res: Response, ): Promise { 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, - res: Response, + res: Response, ): Promise { 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, - res: Response, + res: Response, ): Promise { 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, - res: Response, + res: Response, ): Promise { 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, ): Promise { 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, - res: Response, + res: Response, ): Promise { this.logger.info('Getting strategy parameters'); const { strategyId } = req.params; diff --git a/src/lib/routes/controller.ts b/src/lib/routes/controller.ts index ef8a8e824e..923c8945d7 100644 --- a/src/lib/routes/controller.ts +++ b/src/lib/routes/controller.ts @@ -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; diff --git a/src/lib/services/feature-toggle-service.ts b/src/lib/services/feature-toggle-service.ts index 583c99ad88..68d5e079b4 100644 --- a/src/lib/services/feature-toggle-service.ts +++ b/src/lib/services/feature-toggle-service.ts @@ -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; diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 104a3c7776..9f85b69a37 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -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'; diff --git a/src/lib/services/segment-service.ts b/src/lib/services/segment-service.ts index 79c1235565..bb2748fd7d 100644 --- a/src/lib/services/segment-service.ts +++ b/src/lib/services/segment-service.ts @@ -69,7 +69,7 @@ export class SegmentService { async create(data: unknown, user: User): Promise { 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 { 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): void { + private static validateSegmentValuesLimit( + segment: Omit, + ): void { const limit = SEGMENT_VALUES_LIMIT; const valuesCount = segment.constraints diff --git a/src/lib/types/allowed-strings.ts b/src/lib/types/allowed-strings.ts new file mode 100644 index 0000000000..d633b645c3 --- /dev/null +++ b/src/lib/types/allowed-strings.ts @@ -0,0 +1,4 @@ +// Create a string with allowed values from a values array. ['A', 'B'] => 'A' | 'B' +export type AllowedStrings> = + // eslint-disable-next-line @typescript-eslint/no-shadow + T extends ReadonlyArray ? AllowedStrings : never; diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index f06a6c6489..9e5fd0100d 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -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; export interface IConstraint { contextName: string; - operator: string; + operator: Operator; values?: string[]; value?: string; inverted?: boolean; diff --git a/src/lib/util/constants.ts b/src/lib/util/constants.ts index 6b95bc9e76..1723f54a76 100644 --- a/src/lib/util/constants.ts +++ b/src/lib/util/constants.ts @@ -40,7 +40,7 @@ export const ALL_OPERATORS = [ SEMVER_EQ, SEMVER_GT, SEMVER_LT, -]; +] as const; export const STRING_OPERATORS = [ STR_ENDS_WITH, diff --git a/src/lib/util/serialize-dates.test.ts b/src/lib/util/serialize-dates.test.ts deleted file mode 100644 index 6ef675ff31..0000000000 --- a/src/lib/util/serialize-dates.test.ts +++ /dev/null @@ -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'); -}); diff --git a/src/lib/util/serialize-dates.ts b/src/lib/util/serialize-dates.ts deleted file mode 100644 index ee0755b50f..0000000000 --- a/src/lib/util/serialize-dates.ts +++ /dev/null @@ -1,22 +0,0 @@ -type SerializedDates = { - [P in keyof T]: T[P] extends Date ? string : T[P]; -}; - -// Disallow array arguments for serializeDates. -// Use `array.map(serializeDates)` instead. -type NotArray = Exclude; - -// Serialize top-level date values to strings. -export const serializeDates = ( - obj: NotArray, -): SerializedDates => { - const entries = Object.entries(obj).map(([k, v]) => { - if (v instanceof Date) { - return [k, v.toJSON()]; - } else { - return [k, v]; - } - }); - - return Object.fromEntries(entries); -}; diff --git a/src/test/e2e/api/admin/feature.e2e.test.ts b/src/test/e2e/api/admin/feature.e2e.test.ts index ec4766d2fc..9fd9bd6855 100644 --- a/src/test/e2e/api/admin/feature.e2e.test.ts +++ b/src/test/e2e/api/admin/feature.e2e.test.ts @@ -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 = defaultStrategy, + toggle: FeatureSchema, + strategy: Omit = 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', [ { diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index 5d14cb643f..002e5f3283 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -51,6 +51,21 @@ exports[`should serve the OpenAPI spec 1`] = ` Object { "components": Object { "schemas": Object { + "cloneFeatureSchema": Object { + "properties": Object { + "name": Object { + "type": "string", + }, + "replaceGroupId": Object { + "type": "boolean", + }, + }, + "required": Array [ + "name", + "replaceGroupId", + ], + "type": "object", + }, "constraintSchema": Object { "additionalProperties": false, "properties": Object { @@ -64,6 +79,23 @@ Object { "type": "boolean", }, "operator": Object { + "enum": Array [ + "NOT_IN", + "IN", + "STR_ENDS_WITH", + "STR_STARTS_WITH", + "STR_CONTAINS", + "NUM_EQ", + "NUM_GT", + "NUM_GTE", + "NUM_LT", + "NUM_LTE", + "DATE_AFTER", + "DATE_BEFORE", + "SEMVER_EQ", + "SEMVER_GT", + "SEMVER_LT", + ], "type": "string", }, "value": Object { @@ -107,35 +139,7 @@ Object { "properties": Object { "constraints": Object { "items": Object { - "additionalProperties": false, - "properties": Object { - "caseInsensitive": Object { - "type": "boolean", - }, - "contextName": Object { - "type": "string", - }, - "inverted": Object { - "type": "boolean", - }, - "operator": Object { - "type": "string", - }, - "value": Object { - "type": "string", - }, - "values": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - }, - "required": Array [ - "contextName", - "operator", - ], - "type": "object", + "$ref": "#/components/schemas/constraintSchema", }, "type": "array", }, @@ -143,11 +147,7 @@ Object { "type": "string", }, "parameters": Object { - "additionalProperties": Object { - "maxLength": 100, - "type": "string", - }, - "type": "object", + "$ref": "#/components/schemas/parametersSchema", }, "sortOrder": Object { "type": "number", @@ -155,9 +155,46 @@ Object { }, "type": "object", }, + "emptyResponseSchema": Object { + "description": "OK", + "type": "object", + }, + "featureEnvironmentInfoSchema": Object { + "additionalProperties": false, + "properties": Object { + "enabled": Object { + "type": "boolean", + }, + "environment": Object { + "type": "string", + }, + "name": Object { + "type": "string", + }, + "strategies": Object { + "items": Object { + "$ref": "#/components/schemas/featureStrategySchema", + }, + "type": "array", + }, + "type": Object { + "type": "string", + }, + }, + "required": Array [ + "name", + "environment", + "enabled", + "strategies", + ], + "type": "object", + }, "featureSchema": Object { "additionalProperties": false, "properties": Object { + "archived": Object { + "type": "boolean", + }, "createdAt": Object { "format": "date", "nullable": true, @@ -169,6 +206,12 @@ Object { "enabled": Object { "type": "boolean", }, + "environments": Object { + "items": Object { + "$ref": "#/components/schemas/featureEnvironmentInfoSchema", + }, + "type": "array", + }, "impressionData": Object { "type": "boolean", }, @@ -188,63 +231,7 @@ Object { }, "strategies": Object { "items": Object { - "additionalProperties": false, - "properties": Object { - "constraints": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "caseInsensitive": Object { - "type": "boolean", - }, - "contextName": Object { - "type": "string", - }, - "inverted": Object { - "type": "boolean", - }, - "operator": Object { - "type": "string", - }, - "value": Object { - "type": "string", - }, - "values": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - }, - "required": Array [ - "contextName", - "operator", - ], - "type": "object", - }, - "type": "array", - }, - "id": Object { - "type": "string", - }, - "name": Object { - "type": "string", - }, - "parameters": Object { - "additionalProperties": Object { - "maxLength": 100, - "type": "string", - }, - "type": "object", - }, - }, - "required": Array [ - "id", - "name", - "constraints", - "parameters", - ], - "type": "object", + "$ref": "#/components/schemas/strategySchema", }, "type": "array", }, @@ -253,53 +240,7 @@ Object { }, "variants": Object { "items": Object { - "additionalProperties": false, - "properties": Object { - "name": Object { - "type": "string", - }, - "overrides": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "contextName": Object { - "type": "string", - }, - "values": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - }, - "required": Array [ - "contextName", - "values", - ], - "type": "object", - }, - "type": "array", - }, - "payload": Object { - "type": "object", - }, - "stickiness": Object { - "type": "string", - }, - "weight": Object { - "type": "number", - }, - "weightType": Object { - "type": "string", - }, - }, - "required": Array [ - "name", - "weight", - "weightType", - "stickiness", - ], - "type": "object", + "$ref": "#/components/schemas/variantSchema", }, "type": "array", }, @@ -310,164 +251,62 @@ Object { ], "type": "object", }, + "featureStrategySchema": Object { + "additionalProperties": false, + "properties": Object { + "constraints": Object { + "items": Object { + "$ref": "#/components/schemas/constraintSchema", + }, + "type": "array", + }, + "createdAt": Object { + "format": "date", + "nullable": true, + "type": "string", + }, + "environment": Object { + "type": "string", + }, + "featureName": Object { + "type": "string", + }, + "id": Object { + "type": "string", + }, + "name": Object { + "type": "string", + }, + "parameters": Object { + "$ref": "#/components/schemas/parametersSchema", + }, + "projectId": Object { + "type": "string", + }, + "sortOrder": Object { + "type": "number", + }, + "strategyName": Object { + "type": "string", + }, + }, + "required": Array [ + "id", + "name", + "featureName", + "strategyName", + "constraints", + "parameters", + "environment", + ], + "type": "object", + }, "featuresSchema": Object { "additionalProperties": false, "properties": Object { "features": Object { "items": Object { - "additionalProperties": false, - "properties": Object { - "createdAt": Object { - "format": "date", - "nullable": true, - "type": "string", - }, - "description": Object { - "type": "string", - }, - "enabled": Object { - "type": "boolean", - }, - "impressionData": Object { - "type": "boolean", - }, - "lastSeenAt": Object { - "format": "date", - "nullable": true, - "type": "string", - }, - "name": Object { - "type": "string", - }, - "project": Object { - "type": "string", - }, - "stale": Object { - "type": "boolean", - }, - "strategies": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "constraints": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "caseInsensitive": Object { - "type": "boolean", - }, - "contextName": Object { - "type": "string", - }, - "inverted": Object { - "type": "boolean", - }, - "operator": Object { - "type": "string", - }, - "value": Object { - "type": "string", - }, - "values": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - }, - "required": Array [ - "contextName", - "operator", - ], - "type": "object", - }, - "type": "array", - }, - "id": Object { - "type": "string", - }, - "name": Object { - "type": "string", - }, - "parameters": Object { - "additionalProperties": Object { - "maxLength": 100, - "type": "string", - }, - "type": "object", - }, - }, - "required": Array [ - "id", - "name", - "constraints", - "parameters", - ], - "type": "object", - }, - "type": "array", - }, - "type": Object { - "type": "string", - }, - "variants": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "name": Object { - "type": "string", - }, - "overrides": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "contextName": Object { - "type": "string", - }, - "values": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - }, - "required": Array [ - "contextName", - "values", - ], - "type": "object", - }, - "type": "array", - }, - "payload": Object { - "type": "object", - }, - "stickiness": Object { - "type": "string", - }, - "weight": Object { - "type": "number", - }, - "weightType": Object { - "type": "string", - }, - }, - "required": Array [ - "name", - "weight", - "weightType", - "stickiness", - ], - "type": "object", - }, - "type": "array", - }, - }, - "required": Array [ - "name", - "project", - ], - "type": "object", + "$ref": "#/components/schemas/featureSchema", }, "type": "array", }, @@ -507,40 +346,38 @@ Object { }, "type": "object", }, + "patchOperationSchema": Object { + "properties": Object { + "from": Object { + "type": "string", + }, + "op": Object { + "enum": Array [ + "add", + "remove", + "replace", + "copy", + "move", + ], + "type": "string", + }, + "path": Object { + "type": "string", + }, + "value": Object {}, + }, + "required": Array [ + "path", + "op", + ], + "type": "object", + }, "strategySchema": Object { "additionalProperties": false, "properties": Object { "constraints": Object { "items": Object { - "additionalProperties": false, - "properties": Object { - "caseInsensitive": Object { - "type": "boolean", - }, - "contextName": Object { - "type": "string", - }, - "inverted": Object { - "type": "boolean", - }, - "operator": Object { - "type": "string", - }, - "value": Object { - "type": "string", - }, - "values": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - }, - "required": Array [ - "contextName", - "operator", - ], - "type": "object", + "$ref": "#/components/schemas/constraintSchema", }, "type": "array", }, @@ -551,21 +388,115 @@ Object { "type": "string", }, "parameters": Object { - "additionalProperties": Object { - "maxLength": 100, - "type": "string", - }, - "type": "object", + "$ref": "#/components/schemas/parametersSchema", + }, + "sortOrder": Object { + "type": "number", }, }, "required": Array [ - "id", "name", "constraints", "parameters", ], "type": "object", }, + "tagSchema": Object { + "additionalProperties": false, + "properties": Object { + "type": Object { + "type": "string", + }, + "value": Object { + "type": "string", + }, + }, + "required": Array [ + "value", + "type", + ], + "type": "object", + }, + "tagsResponseSchema": Object { + "additionalProperties": false, + "properties": Object { + "tags": Object { + "items": Object { + "$ref": "#/components/schemas/tagSchema", + }, + "type": "array", + }, + "version": Object { + "type": "integer", + }, + }, + "required": Array [ + "version", + "tags", + ], + "type": "object", + }, + "updateFeatureSchema": Object { + "properties": Object { + "archived": Object { + "type": "boolean", + }, + "constraints": Object { + "items": Object { + "$ref": "#/components/schemas/constraintSchema", + }, + "type": "array", + }, + "createdAt": Object { + "format": "date", + "type": "string", + }, + "description": Object { + "type": "string", + }, + "impressionData": Object { + "type": "boolean", + }, + "name": Object { + "type": "string", + }, + "stale": Object { + "type": "boolean", + }, + "type": Object { + "type": "string", + }, + }, + "required": Array [ + "name", + ], + "type": "object", + }, + "updateStrategySchema": Object { + "additionalProperties": false, + "properties": Object { + "constraints": Object { + "items": Object { + "$ref": "#/components/schemas/constraintSchema", + }, + "type": "array", + }, + "id": Object { + "type": "string", + }, + "name": Object { + "type": "string", + }, + "parameters": Object { + "$ref": "#/components/schemas/parametersSchema", + }, + "sortOrder": Object { + "type": "number", + }, + }, + "required": Array [], + "type": "object", + }, "variantSchema": Object { "additionalProperties": false, "properties": Object { @@ -574,27 +505,23 @@ Object { }, "overrides": Object { "items": Object { - "additionalProperties": false, - "properties": Object { - "contextName": Object { - "type": "string", - }, - "values": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - }, - "required": Array [ - "contextName", - "values", - ], - "type": "object", + "$ref": "#/components/schemas/overrideSchema", }, "type": "array", }, "payload": Object { + "properties": Object { + "type": Object { + "type": "string", + }, + "value": Object { + "type": "string", + }, + }, + "required": Array [ + "type", + "value", + ], "type": "object", }, "stickiness": Object { @@ -682,6 +609,7 @@ Object { "/api/admin/features": Object { "get": Object { "deprecated": true, + "operationId": "getAllToggles", "responses": Object { "200": Object { "content": Object { @@ -699,8 +627,143 @@ Object { ], }, }, + "/api/admin/features/validate": Object { + "post": Object { + "operationId": "validateFeature", + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/emptyResponseSchema", + }, + }, + }, + "description": "emptyResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, + "/api/admin/features/{featureName}/tags": Object { + "get": Object { + "operationId": "listTags", + "parameters": Array [ + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/tagsResponseSchema", + }, + }, + }, + "description": "tagsResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + "post": Object { + "operationId": "addTag", + "parameters": Array [ + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/tagSchema", + }, + }, + }, + "required": true, + }, + "responses": Object { + "201": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/tagSchema", + }, + }, + }, + "description": "tagResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, + "/api/admin/features/{featureName}/tags/{type}/{value}": Object { + "delete": Object { + "operationId": "removeTag", + "parameters": Array [ + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "type", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "value", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/emptyResponseSchema", + }, + }, + }, + "description": "emptyResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, "/api/admin/projects/{projectId}/features": Object { "get": Object { + "operationId": "getFeatures", "parameters": Array [ Object { "in": "path", @@ -728,6 +791,7 @@ Object { ], }, "post": Object { + "operationId": "createFeature", "parameters": Array [ Object { "in": "path", @@ -766,7 +830,44 @@ Object { }, }, "/api/admin/projects/{projectId}/features/{featureName}": Object { + "delete": Object { + "operationId": "archiveFeature", + "parameters": Array [ + Object { + "in": "path", + "name": "projectId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/emptyResponseSchema", + }, + }, + }, + "description": "emptyResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, "get": Object { + "operationId": "getFeature", "parameters": Array [ Object { "in": "path", @@ -801,9 +902,338 @@ Object { "admin", ], }, + "patch": Object { + "operationId": "patchFeature", + "parameters": Array [ + Object { + "in": "path", + "name": "projectId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "items": Object { + "$ref": "#/components/schemas/patchOperationSchema", + }, + "type": "array", + }, + }, + }, + "required": true, + }, + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/featureSchema", + }, + }, + }, + "description": "featureResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + "put": Object { + "operationId": "updateFeature", + "parameters": Array [ + Object { + "in": "path", + "name": "projectId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/updateFeatureSchema", + }, + }, + }, + "required": true, + }, + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/featureSchema", + }, + }, + }, + "description": "featureResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, + "/api/admin/projects/{projectId}/features/{featureName}/clone": Object { + "post": Object { + "operationId": "cloneFeature", + "parameters": Array [ + Object { + "in": "path", + "name": "projectId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/cloneFeatureSchema", + }, + }, + }, + "required": true, + }, + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/featureSchema", + }, + }, + }, + "description": "featureResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, + "/api/admin/projects/{projectId}/features/{featureName}/environments/{environment}": Object { + "get": Object { + "operationId": "getEnvironment", + "parameters": Array [ + Object { + "in": "path", + "name": "projectId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "environment", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/featureEnvironmentInfoSchema", + }, + }, + }, + "description": "featureEnvironmentInfoResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, + "/api/admin/projects/{projectId}/features/{featureName}/environments/{environment}/off": Object { + "post": Object { + "operationId": "toggleEnvironmentOff", + "parameters": Array [ + Object { + "in": "path", + "name": "projectId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "environment", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/featureSchema", + }, + }, + }, + "description": "featureResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, + "/api/admin/projects/{projectId}/features/{featureName}/environments/{environment}/on": Object { + "post": Object { + "operationId": "toggleEnvironmentOn", + "parameters": Array [ + Object { + "in": "path", + "name": "projectId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "environment", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/featureSchema", + }, + }, + }, + "description": "featureResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, }, "/api/admin/projects/{projectId}/features/{featureName}/environments/{environment}/strategies": Object { + "get": Object { + "operationId": "getStrategies", + "parameters": Array [ + Object { + "in": "path", + "name": "projectId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "environment", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "items": Object { + "$ref": "#/components/schemas/strategySchema", + }, + "type": "array", + }, + }, + }, + "description": "strategiesResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, "post": Object { + "operationId": "addStrategy", "parameters": Array [ Object { "in": "path", @@ -845,7 +1275,7 @@ Object { "content": Object { "application/json": Object { "schema": Object { - "$ref": "#/components/schemas/strategySchema", + "$ref": "#/components/schemas/featureStrategySchema", }, }, }, @@ -858,7 +1288,112 @@ Object { }, }, "/api/admin/projects/{projectId}/features/{featureName}/environments/{environment}/strategies/{strategyId}": Object { - "put": Object { + "delete": Object { + "operationId": "deleteStrategy", + "parameters": Array [ + Object { + "in": "path", + "name": "projectId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "environment", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "strategyId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/emptyResponseSchema", + }, + }, + }, + "description": "emptyResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + "get": Object { + "operationId": "getStrategy", + "parameters": Array [ + Object { + "in": "path", + "name": "projectId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "environment", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "strategyId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/featureStrategySchema", + }, + }, + }, + "description": "strategyResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + "patch": Object { + "operationId": "patchStrategy", "parameters": Array [ Object { "in": "path", @@ -897,7 +1432,10 @@ Object { "content": Object { "application/json": Object { "schema": Object { - "$ref": "#/components/schemas/createStrategySchema", + "items": Object { + "$ref": "#/components/schemas/patchOperationSchema", + }, + "type": "array", }, }, }, @@ -908,7 +1446,69 @@ Object { "content": Object { "application/json": Object { "schema": Object { - "$ref": "#/components/schemas/strategySchema", + "$ref": "#/components/schemas/featureStrategySchema", + }, + }, + }, + "description": "strategyResponse", + }, + }, + "tags": Array [ + "admin", + ], + }, + "put": Object { + "operationId": "updateStrategy", + "parameters": Array [ + Object { + "in": "path", + "name": "projectId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "featureName", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "environment", + "required": true, + "schema": Object { + "type": "string", + }, + }, + Object { + "in": "path", + "name": "strategyId", + "required": true, + "schema": Object { + "type": "string", + }, + }, + ], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/updateStrategySchema", + }, + }, + }, + "required": true, + }, + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/featureStrategySchema", }, }, }, diff --git a/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts b/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts index 67025897f5..30bf3fdfcb 100644 --- a/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts +++ b/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts @@ -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 = { + const config: Omit = { 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 = { + const config: Omit = { 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 = { + const config: Omit = { name: 'default', constraints: [], parameters: {}, diff --git a/src/test/fixtures/fake-feature-strategies-store.ts b/src/test/fixtures/fake-feature-strategies-store.ts index 5dad977a64..a343a11188 100644 --- a/src/test/fixtures/fake-feature-strategies-store.ts +++ b/src/test/fixtures/fake-feature-strategies-store.ts @@ -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'; diff --git a/src/test/fixtures/fake-feature-toggle-store.ts b/src/test/fixtures/fake-feature-toggle-store.ts index afdf5722bf..412269ee6a 100644 --- a/src/test/fixtures/fake-feature-toggle-store.ts +++ b/src/test/fixtures/fake-feature-toggle-store.ts @@ -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 { + async create(project: string, data: FeatureToggle): Promise { 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 { const feature = await this.get(featureName); - return feature.variants; + return feature.variants as IVariant[]; } async saveVariants(