From 2da279b7fbda59f8d8199e4f4fdc2e86d0c1cea8 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Tue, 18 Apr 2023 09:59:02 +0300 Subject: [PATCH] feat: add title to strategy (#3510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds title column to strategies, feature_strategies and features_view in the db Updates model/schemas ## About the changes Closes # [1-855](https://linear.app/unleash/issue/1-855/allow-for-title-on-strategy-backend) ### Important files ## Discussion points --------- Signed-off-by: andreas-unleash --- src/lib/db/feature-strategy-store.ts | 27 ++++-- src/lib/db/strategy-store.ts | 6 ++ .../spec/create-feature-strategy-schema.ts | 27 ++++++ .../openapi/spec/feature-strategy-schema.ts | 19 +++++ src/lib/openapi/spec/strategy-schema.test.ts | 6 ++ src/lib/openapi/spec/strategy-schema.ts | 23 +++++ src/lib/services/feature-toggle-service.ts | 44 +++++----- src/lib/services/strategy-schema.ts | 1 + src/lib/types/model.ts | 2 + src/lib/types/stores/strategy-store.ts | 5 ++ .../feature-evaluator/strategy/strategy.ts | 3 +- .../20230412125618-add-title-to-strategy.js | 84 +++++++++++++++++++ src/test/e2e/api/admin/strategy.e2e.test.ts | 51 +++++++++++ .../__snapshots__/openapi.e2e.test.ts.snap | 76 +++++++++++++++++ .../feature-toggle-service-v2.e2e.test.ts | 16 ++-- 15 files changed, 354 insertions(+), 36 deletions(-) create mode 100644 src/migrations/20230412125618-add-title-to-strategy.js diff --git a/src/lib/db/feature-strategy-store.ts b/src/lib/db/feature-strategy-store.ts index 56dc2044df..c7f93ba441 100644 --- a/src/lib/db/feature-strategy-store.ts +++ b/src/lib/db/feature-strategy-store.ts @@ -10,20 +10,20 @@ import { IConstraint, IEnvironmentOverview, IFeatureOverview, + IFeatureStrategiesStore, IFeatureStrategy, IFeatureToggleClient, + IFlagResolver, IStrategyConfig, ITag, -} from '../types/model'; -import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store'; -import { PartialDeep, PartialSome } from '../types/partial'; + PartialDeep, + PartialSome, +} from '../types'; import FeatureToggleStore from './feature-toggle-store'; -import { ensureStringValue } from '../util/ensureStringValue'; -import { mapValues } from '../util/map-values'; -import { IFlagResolver } from '../types/experimental'; +import { ensureStringValue, mapValues } from '../util'; import { IFeatureProjectUserParams } from '../routes/admin-api/project/project-features'; -import Raw = Knex.Raw; import { Db } from './db'; +import Raw = Knex.Raw; const COLUMNS = [ 'id', @@ -31,6 +31,7 @@ const COLUMNS = [ 'project_name', 'environment', 'strategy_name', + 'title', 'parameters', 'constraints', 'created_at', @@ -55,6 +56,7 @@ interface IFeatureStrategiesTable { feature_name: string; project_name: string; environment: string; + title?: string | null; strategy_name: string; parameters: object; constraints: string; @@ -76,6 +78,7 @@ function mapRow(row: IFeatureStrategiesTable): IFeatureStrategy { projectId: row.project_name, environment: row.environment, strategyName: row.strategy_name, + title: row.title, parameters: mapValues(row.parameters || {}, ensureStringValue), constraints: (row.constraints as unknown as IConstraint[]) || [], createdAt: row.created_at, @@ -90,6 +93,7 @@ function mapInput(input: IFeatureStrategy): IFeatureStrategiesTable { project_name: input.projectId, environment: input.environment, strategy_name: input.strategyName, + title: input.title, parameters: input.parameters, constraints: JSON.stringify(input.constraints || []), created_at: input.createdAt, @@ -101,6 +105,7 @@ interface StrategyUpdate { strategy_name: string; parameters: object; constraints: string; + title?: string; } function mapStrategyUpdate( @@ -113,6 +118,9 @@ function mapStrategyUpdate( if (input.parameters !== null) { update.parameters = input.parameters; } + if (input.title !== null) { + update.title = input.title; + } update.constraints = JSON.stringify(input.constraints || []); return update; } @@ -376,8 +384,8 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { feature: PartialDeep, row: Record, ) { - const strategy = feature.strategies.find( - (s) => s.id === row.strategy_id, + const strategy = feature.strategies?.find( + (s) => s?.id === row.strategy_id, ); if (!strategy) { return; @@ -581,6 +589,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { parameters: r.parameters, sortOrder: r.sort_order, id: r.strategy_id, + title: r.strategy_title || '', }; if (!includeId) { delete strategy.id; diff --git a/src/lib/db/strategy-store.ts b/src/lib/db/strategy-store.ts index fc5237c399..e99ae9ef99 100644 --- a/src/lib/db/strategy-store.ts +++ b/src/lib/db/strategy-store.ts @@ -11,6 +11,7 @@ import { import { Db } from './db'; const STRATEGY_COLUMNS = [ + 'title', 'name', 'description', 'parameters', @@ -21,6 +22,7 @@ const STRATEGY_COLUMNS = [ const TABLE = 'strategies'; interface IStrategyRow { + title: string; name: string; built_in: number; description: string; @@ -109,6 +111,7 @@ export default class StrategyStore implements IStrategyStore { description: row.description, parameters: row.parameters, deprecated: row.deprecated, + title: row.title, }; } @@ -121,6 +124,7 @@ export default class StrategyStore implements IStrategyStore { description: row.description, parameters: row.parameters, deprecated: row.deprecated, + title: row.title, }; } @@ -130,6 +134,7 @@ export default class StrategyStore implements IStrategyStore { name: data.name, description: data.description, parameters: JSON.stringify(data.parameters), + title: data.title, }; } @@ -166,6 +171,7 @@ export default class StrategyStore implements IStrategyStore { built_in: data.builtIn ? 1 : 0, sort_order: data.sortOrder || 9999, display_name: data.displayName, + title: data.title, }; await this.db(TABLE).insert(rowData).onConflict(['name']).merge(); } diff --git a/src/lib/openapi/spec/create-feature-strategy-schema.ts b/src/lib/openapi/spec/create-feature-strategy-schema.ts index 56df6a795c..12e6359500 100644 --- a/src/lib/openapi/spec/create-feature-strategy-schema.ts +++ b/src/lib/openapi/spec/create-feature-strategy-schema.ts @@ -9,22 +9,49 @@ export const createFeatureStrategySchema = { properties: { name: { type: 'string', + description: 'The name or type of strategy', + example: 'flexibleRollout', + }, + title: { + type: 'string', + nullable: true, + description: 'A descriptive title for the strategy', + example: 'Gradual Rollout 25-Prod', }, sortOrder: { type: 'number', + description: 'The order of the strategy in the list', + example: 9999, }, constraints: { type: 'array', + description: 'A list of the constraints attached to the strategy', + example: [ + { + values: ['1', '2'], + inverted: false, + operator: 'IN', + contextName: 'appName', + caseInsensitive: false, + }, + ], items: { $ref: '#/components/schemas/constraintSchema', }, }, parameters: { + description: 'An object containing the parameters for the strategy', + example: { + groupId: 'some_new', + rollout: '25', + stickiness: 'sessionId', + }, $ref: '#/components/schemas/parametersSchema', }, segments: { type: 'array', description: 'Ids of segments to use for this strategy', + example: [1, 2], items: { type: 'number', }, diff --git a/src/lib/openapi/spec/feature-strategy-schema.ts b/src/lib/openapi/spec/feature-strategy-schema.ts index cb26805dcb..d7979967fb 100644 --- a/src/lib/openapi/spec/feature-strategy-schema.ts +++ b/src/lib/openapi/spec/feature-strategy-schema.ts @@ -4,30 +4,49 @@ import { parametersSchema } from './parameters-schema'; export const featureStrategySchema = { $id: '#/components/schemas/featureStrategySchema', + description: + 'A singles activation strategy configuration schema for a feature', type: 'object', additionalProperties: false, required: ['name'], properties: { id: { type: 'string', + description: 'A uuid for the feature strategy', + example: '6b5157cb-343a-41e7-bfa3-7b4ec3044840', }, name: { type: 'string', + description: 'The name or type of strategy', + example: 'flexibleRollout', + }, + title: { + type: 'string', + description: 'A descriptive title for the strategy', + example: 'Gradual Rollout 25-Prod', + nullable: true, }, featureName: { type: 'string', + description: 'The name or feature the strategy is attached to', + example: 'myAwesomeFeature', }, sortOrder: { type: 'number', + description: 'The order of the strategy in the list', + example: 9999, }, segments: { type: 'array', + description: 'A list of segment ids attached to the strategy', + example: [1, 2], items: { type: 'number', }, }, constraints: { type: 'array', + description: 'A list of the constraints attached to the strategy', items: { $ref: '#/components/schemas/constraintSchema', }, diff --git a/src/lib/openapi/spec/strategy-schema.test.ts b/src/lib/openapi/spec/strategy-schema.test.ts index b12252891c..59121c490f 100644 --- a/src/lib/openapi/spec/strategy-schema.test.ts +++ b/src/lib/openapi/spec/strategy-schema.test.ts @@ -4,6 +4,7 @@ import { StrategySchema } from './strategy-schema'; test('strategySchema', () => { const data: StrategySchema = { description: '', + title: '', name: '', displayName: '', editable: false, @@ -25,4 +26,9 @@ test('strategySchema', () => { expect( validateSchema('#/components/schemas/strategySchema', {}), ).toMatchSnapshot(); + + const { title, ...noTitle } = { ...data }; + expect( + validateSchema('#/components/schemas/strategySchema', noTitle), + ).toBeUndefined(); }); diff --git a/src/lib/openapi/spec/strategy-schema.ts b/src/lib/openapi/spec/strategy-schema.ts index 2908d48cc3..0282bbfb31 100644 --- a/src/lib/openapi/spec/strategy-schema.ts +++ b/src/lib/openapi/spec/strategy-schema.ts @@ -2,6 +2,8 @@ import { FromSchema } from 'json-schema-to-ts'; export const strategySchema = { $id: '#/components/schemas/strategySchema', + description: + 'The [activation strategy](https://docs.getunleash.io/reference/activation-strategies) schema', type: 'object', additionalProperties: false, required: [ @@ -13,39 +15,60 @@ export const strategySchema = { 'parameters', ], properties: { + title: { + type: 'string', + nullable: true, + description: 'An optional title for the strategy', + example: 'GradualRollout - Prod25', + }, name: { type: 'string', + description: 'The name or type of the strategy', + example: 'flexibleRollout', }, displayName: { type: 'string', + description: 'A human friendly name for the strategy', + example: 'Gradual Rollout', nullable: true, }, description: { type: 'string', + description: 'A short description for the strategy', + example: 'Gradual rollout to logged in users', }, editable: { type: 'boolean', + description: 'Determines whether the strategy allows for editing', + example: true, }, deprecated: { type: 'boolean', + description: '', + example: true, }, parameters: { type: 'array', + description: 'A list of relevant parameters for each strategy', items: { type: 'object', additionalProperties: false, properties: { name: { type: 'string', + example: 'percentage', }, type: { type: 'string', + example: 'percentage', }, description: { type: 'string', + example: 'Gradual rollout to logged in users', }, required: { type: 'boolean', + example: true, }, }, }, diff --git a/src/lib/services/feature-toggle-service.ts b/src/lib/services/feature-toggle-service.ts index 14b6c4bfc6..7255666ce0 100644 --- a/src/lib/services/feature-toggle-service.ts +++ b/src/lib/services/feature-toggle-service.ts @@ -1,17 +1,5 @@ -import { IUnleashConfig } from '../types/option'; -import { IFlagResolver, IUnleashStores } from '../types'; -import { Logger } from '../logger'; -import BadDataError from '../error/bad-data-error'; -import NameExistsError from '../error/name-exists-error'; -import InvalidOperationError from '../error/invalid-operation-error'; -import { FOREIGN_KEY_VIOLATION } from '../error/db-error'; -import { - constraintSchema, - featureMetadataSchema, - nameSchema, - variantsArraySchema, -} from '../schema/feature-schema'; import { + EnvironmentVariantEvent, FEATURE_UPDATED, FeatureArchivedEvent, FeatureChangeProjectEvent, @@ -25,17 +13,30 @@ import { FeatureStrategyRemoveEvent, FeatureStrategyUpdateEvent, FeatureVariantEvent, - EnvironmentVariantEvent, -} from '../types/events'; + IEventStore, + IFeatureTagStore, + IFeatureToggleStore, + IFlagResolver, + IProjectStore, + IUnleashConfig, + IUnleashStores, +} from '../types'; +import { Logger } from '../logger'; +import BadDataError from '../error/bad-data-error'; +import NameExistsError from '../error/name-exists-error'; +import InvalidOperationError from '../error/invalid-operation-error'; +import { FOREIGN_KEY_VIOLATION } from '../error'; +import { + constraintSchema, + featureMetadataSchema, + nameSchema, + variantsArraySchema, +} from '../schema/feature-schema'; import NotFoundError from '../error/notfound-error'; import { FeatureConfigurationClient, IFeatureStrategiesStore, } from '../types/stores/feature-strategies-store'; -import { IEventStore } from '../types/stores/event-store'; -import { IProjectStore } from '../types/stores/project-store'; -import { IFeatureTagStore } from '../types/stores/feature-tag-store'; -import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; import { FeatureToggle, FeatureToggleDTO, @@ -354,6 +355,7 @@ class FeatureToggleService { return { id: featureStrategy.id, name: featureStrategy.strategyName, + title: featureStrategy.title, constraints: featureStrategy.constraints || [], parameters: featureStrategy.parameters, segments: segments.map((segment) => segment.id) ?? [], @@ -415,6 +417,7 @@ class FeatureToggleService { const newFeatureStrategy = await this.featureStrategiesStore.createStrategyFeatureEnv({ strategyName: strategyConfig.name, + title: strategyConfig.title, constraints: strategyConfig.constraints || [], parameters: strategyConfig.parameters || {}, sortOrder: strategyConfig.sortOrder, @@ -598,6 +601,7 @@ class FeatureToggleService { * @param id - strategy id * @param context - Which context does this strategy live in (projectId, featureName, environment) * @param createdBy - Which user does this strategy belong to + * @param user */ async deleteStrategy( id: string, @@ -689,6 +693,7 @@ class FeatureToggleService { * @param featureName * @param archived - return archived or non archived toggles * @param projectId - provide if you're requesting the feature in the context of a specific project. + * @param userId */ async getFeature({ featureName, @@ -1007,6 +1012,7 @@ class FeatureToggleService { constraints: strategy.constraints || [], parameters: strategy.parameters, segments: [], + title: strategy.title, }; if (segments && segments.length > 0) { diff --git a/src/lib/services/strategy-schema.ts b/src/lib/services/strategy-schema.ts index ea4e55af32..008d30fbda 100644 --- a/src/lib/services/strategy-schema.ts +++ b/src/lib/services/strategy-schema.ts @@ -5,6 +5,7 @@ const strategySchema = joi .object() .keys({ name: nameType, + title: joi.string().allow(null).allow('').optional(), editable: joi.boolean().default(true), deprecated: joi.boolean().default(false), description: joi.string().allow(null).allow('').optional(), diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index c61fe53844..5bdeb8647b 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -27,6 +27,7 @@ export interface IStrategyConfig { segments?: number[]; parameters?: { [key: string]: string }; sortOrder?: number; + title?: string | null; } export interface IFeatureStrategy { id: string; @@ -39,6 +40,7 @@ export interface IFeatureStrategy { constraints: IConstraint[]; createdAt?: Date; segments?: number[]; + title?: string | null; } export interface FeatureToggleDTO { diff --git a/src/lib/types/stores/strategy-store.ts b/src/lib/types/stores/strategy-store.ts index 2433a37eb5..cb2b4412c4 100644 --- a/src/lib/types/stores/strategy-store.ts +++ b/src/lib/types/stores/strategy-store.ts @@ -7,6 +7,7 @@ export interface IStrategy { parameters: object[]; deprecated: boolean; displayName: string; + title?: string; } export interface IEditableStrategy { @@ -14,6 +15,7 @@ export interface IEditableStrategy { description?: string; parameters: object; deprecated: boolean; + title?: string; } export interface IMinimalStrategy { @@ -21,6 +23,7 @@ export interface IMinimalStrategy { description?: string; editable?: boolean; parameters?: any[]; + title?: string; } export interface IStrategyImport { @@ -31,6 +34,7 @@ export interface IStrategyImport { builtIn?: boolean; sortOrder?: number; displayName?: string; + title?: string; } export interface IMinimalStrategyRow { @@ -38,6 +42,7 @@ export interface IMinimalStrategyRow { description?: string; editable?: boolean; parameters?: string; + title?: string; } export interface IStrategyStore extends Store { diff --git a/src/lib/util/feature-evaluator/strategy/strategy.ts b/src/lib/util/feature-evaluator/strategy/strategy.ts index 2aad945d63..76fe37caa0 100644 --- a/src/lib/util/feature-evaluator/strategy/strategy.ts +++ b/src/lib/util/feature-evaluator/strategy/strategy.ts @@ -12,6 +12,7 @@ export type SegmentForEvaluation = { export interface StrategyTransportInterface { name: string; + title?: string; parameters: any; constraints: Constraint[]; segments?: number[]; @@ -62,7 +63,7 @@ export class Strategy { }; } - const mappedConstraints = []; + const mappedConstraints: PlaygroundConstraintSchema[] = []; for (const constraint of constraints) { if (constraint) { mappedConstraints.push({ diff --git a/src/migrations/20230412125618-add-title-to-strategy.js b/src/migrations/20230412125618-add-title-to-strategy.js new file mode 100644 index 0000000000..1f4e9aba58 --- /dev/null +++ b/src/migrations/20230412125618-add-title-to-strategy.js @@ -0,0 +1,84 @@ +'use strict'; + +exports.up = function (db, callback) { + db.runSql( + ` + ALTER TABLE strategies ADD COLUMN IF NOT EXISTS title TEXT; + ALTER TABLE feature_strategies ADD COLUMN IF NOT EXISTS title TEXT; + + CREATE OR REPLACE VIEW features_view AS + SELECT + features.name as name, + features.description as description, + features.type as type, + features.project as project, + features.stale as stale, + feature_environments.variants as variants, + features.impression_data as impression_data, + features.created_at as created_at, + features.last_seen_at as last_seen_at, + features.archived_at as archived_at, + feature_environments.enabled as enabled, + feature_environments.environment as environment, + environments.name as environment_name, + environments.type as environment_type, + environments.sort_order as environment_sort_order, + feature_strategies.id as strategy_id, + feature_strategies.strategy_name as strategy_name, + feature_strategies.parameters as parameters, + feature_strategies.constraints as constraints, + feature_strategies.sort_order as sort_order, + fss.segment_id as segments, + feature_strategies.title as strategy_title + FROM + features + LEFT JOIN feature_environments ON feature_environments.feature_name = features.name + LEFT JOIN feature_strategies ON feature_strategies.feature_name = feature_environments.feature_name + and feature_strategies.environment = feature_environments.environment + LEFT JOIN environments ON feature_environments.environment = environments.name + LEFT JOIN feature_strategy_segment as fss ON fss.feature_strategy_id = feature_strategies.id; + `, + callback, + ); +}; + +exports.down = function (db, callback) { + db.runSql( + ` + ALTER TABLE strategies DROP COLUMN IF EXISTS title; + ALTER TABLE feature_strategies DROP COLUMN IF EXISTS title; + DROP VIEW features_view; + CREATE VIEW features_view AS + SELECT + features.name as name, + features.description as description, + features.type as type, + features.project as project, + features.stale as stale, + feature_environments.variants as variants, + features.impression_data as impression_data, + features.created_at as created_at, + features.last_seen_at as last_seen_at, + features.archived_at as archived_at, + feature_environments.enabled as enabled, + feature_environments.environment as environment, + environments.name as environment_name, + environments.type as environment_type, + environments.sort_order as environment_sort_order, + feature_strategies.id as strategy_id, + feature_strategies.strategy_name as strategy_name, + feature_strategies.parameters as parameters, + feature_strategies.constraints as constraints, + feature_strategies.sort_order as sort_order, + fss.segment_id as segments + FROM + features + LEFT JOIN feature_environments ON feature_environments.feature_name = features.name + LEFT JOIN feature_strategies ON feature_strategies.feature_name = feature_environments.feature_name + and feature_strategies.environment = feature_environments.environment + LEFT JOIN environments ON feature_environments.environment = environments.name + LEFT JOIN feature_strategy_segment as fss ON fss.feature_strategy_id = feature_strategies.id; + `, + callback, + ); +}; diff --git a/src/test/e2e/api/admin/strategy.e2e.test.ts b/src/test/e2e/api/admin/strategy.e2e.test.ts index 235f7ccaf3..1d99cde56b 100644 --- a/src/test/e2e/api/admin/strategy.e2e.test.ts +++ b/src/test/e2e/api/admin/strategy.e2e.test.ts @@ -204,3 +204,54 @@ test('can update a exiting strategy with deprecated', async () => { .set('Content-Type', 'application/json') .expect(200); }); + +test('can create a strategy with a title', async () => { + await app.request + .post('/api/admin/strategies') + .send({ + name: 'myCustomStrategyWithTitle', + description: 'Best strategy ever.', + parameters: [], + title: 'This is the best strategy ever', + }) + .set('Content-Type', 'application/json') + .expect(201); + + const { body: strategy } = await app.request.get( + '/api/admin/strategies/myCustomStrategyWithTitle', + ); + + expect(strategy.title).toBe('This is the best strategy ever'); + + strategy.description = 'A new desc'; + + return app.request + .put('/api/admin/strategies/myCustomStrategyWithTitle') + .send(strategy) + .set('Content-Type', 'application/json') + .expect(200); +}); + +test('can update a strategy with a title', async () => { + await app.request + .post('/api/admin/strategies') + .send({ + name: 'myCustomStrategy2', + description: 'Best strategy ever.', + parameters: [], + }) + .set('Content-Type', 'application/json') + .expect(201); + + const { body: strategy } = await app.request.get( + '/api/admin/strategies/myCustomStrategy2', + ); + + strategy.title = 'This is the best strategy ever'; + + return app.request + .put('/api/admin/strategies/myCustomStrategy2') + .send(strategy) + .set('Content-Type', 'application/json') + .expect(200); +}); 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 144974d10c..dd66802c8d 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 @@ -936,27 +936,60 @@ exports[`should serve the OpenAPI spec 1`] = ` "createFeatureStrategySchema": { "properties": { "constraints": { + "description": "A list of the constraints attached to the strategy", + "example": [ + { + "caseInsensitive": false, + "contextName": "appName", + "inverted": false, + "operator": "IN", + "values": [ + "1", + "2", + ], + }, + ], "items": { "$ref": "#/components/schemas/constraintSchema", }, "type": "array", }, "name": { + "description": "The name or type of strategy", + "example": "flexibleRollout", "type": "string", }, "parameters": { "$ref": "#/components/schemas/parametersSchema", + "description": "An object containing the parameters for the strategy", + "example": { + "groupId": "some_new", + "rollout": "25", + "stickiness": "sessionId", + }, }, "segments": { "description": "Ids of segments to use for this strategy", + "example": [ + 1, + 2, + ], "items": { "type": "number", }, "type": "array", }, "sortOrder": { + "description": "The order of the strategy in the list", + "example": 9999, "type": "number", }, + "title": { + "description": "A descriptive title for the strategy", + "example": "Gradual Rollout 25-Prod", + "nullable": true, + "type": "string", + }, }, "required": [ "name", @@ -1628,34 +1661,55 @@ exports[`should serve the OpenAPI spec 1`] = ` }, "featureStrategySchema": { "additionalProperties": false, + "description": "A singles activation strategy configuration schema for a feature", "properties": { "constraints": { + "description": "A list of the constraints attached to the strategy", "items": { "$ref": "#/components/schemas/constraintSchema", }, "type": "array", }, "featureName": { + "description": "The name or feature the strategy is attached to", + "example": "myAwesomeFeature", "type": "string", }, "id": { + "description": "A uuid for the feature strategy", + "example": "6b5157cb-343a-41e7-bfa3-7b4ec3044840", "type": "string", }, "name": { + "description": "The name or type of strategy", + "example": "flexibleRollout", "type": "string", }, "parameters": { "$ref": "#/components/schemas/parametersSchema", }, "segments": { + "description": "A list of segment ids attached to the strategy", + "example": [ + 1, + 2, + ], "items": { "type": "number", }, "type": "array", }, "sortOrder": { + "description": "The order of the strategy in the list", + "example": 9999, "type": "number", }, + "title": { + "description": "A descriptive title for the strategy", + "example": "Gradual Rollout 25-Prod", + "nullable": true, + "type": "string", + }, }, "required": [ "name", @@ -3734,37 +3788,53 @@ Stats are divided into current and previous **windows**. }, "strategySchema": { "additionalProperties": false, + "description": "The [activation strategy](https://docs.getunleash.io/reference/activation-strategies) schema", "properties": { "deprecated": { + "description": "", + "example": true, "type": "boolean", }, "description": { + "description": "A short description for the strategy", + "example": "Gradual rollout to logged in users", "type": "string", }, "displayName": { + "description": "A human friendly name for the strategy", + "example": "Gradual Rollout", "nullable": true, "type": "string", }, "editable": { + "description": "Determines whether the strategy allows for editing", + "example": true, "type": "boolean", }, "name": { + "description": "The name or type of the strategy", + "example": "flexibleRollout", "type": "string", }, "parameters": { + "description": "A list of relevant parameters for each strategy", "items": { "additionalProperties": false, "properties": { "description": { + "example": "Gradual rollout to logged in users", "type": "string", }, "name": { + "example": "percentage", "type": "string", }, "required": { + "example": true, "type": "boolean", }, "type": { + "example": "percentage", "type": "string", }, }, @@ -3772,6 +3842,12 @@ Stats are divided into current and previous **windows**. }, "type": "array", }, + "title": { + "description": "An optional title for the strategy", + "example": "GradualRollout - Prod25", + "nullable": true, + "type": "string", + }, }, "required": [ "name", 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 ab492887ed..25b4370ad3 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,16 +1,17 @@ import FeatureToggleService from '../../../lib/services/feature-toggle-service'; import { createTestConfig } from '../../config/test-config'; import dbInit from '../helpers/database-init'; -import { DEFAULT_ENV } from '../../../lib/util/constants'; -import { SegmentService } from '../../../lib/services/segment-service'; -import { FeatureStrategySchema } from '../../../lib/openapi/spec/feature-strategy-schema'; +import { DEFAULT_ENV } from '../../../lib/util'; +import { + AccessService, + GroupService, + SegmentService, +} from '../../../lib/services'; +import { FeatureStrategySchema } from '../../../lib/openapi'; import User from '../../../lib/types/user'; -import { IConstraint, IVariant } from '../../../lib/types/model'; -import { AccessService } from '../../../lib/services/access-service'; -import { GroupService } from '../../../lib/services/group-service'; +import { IConstraint, IVariant, SKIP_CHANGE_REQUEST } from '../../../lib/types'; import EnvironmentService from '../../../lib/services/environment-service'; import { NoAccessError } from '../../../lib/error'; -import { SKIP_CHANGE_REQUEST } from '../../../lib/types'; import { ISegmentService } from '../../../lib/segments/segment-service-interface'; import { ChangeRequestAccessReadModel } from '../../../lib/features/change-request-access-service/sql-change-request-access-read-model'; @@ -134,6 +135,7 @@ test('Should be able to get strategy by id', async () => { name: 'default', constraints: [], parameters: {}, + title: 'some-title', }; await service.createFeatureToggle( projectId,