1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: add title to strategy (#3510)

<!-- Thanks for creating a PR! To make it easier for reviewers and
everyone else to understand what your changes relate to, please add some
relevant content to the headings below. Feel free to ignore or delete
sections that you don't think are relevant. Thank you! ❤️ -->
Adds title column to strategies, feature_strategies and features_view in
the db
Updates model/schemas
## About the changes
<!-- Describe the changes introduced. What are they and why are they
being introduced? Feel free to also add screenshots or steps to view the
changes if they're visual. -->

<!-- Does it close an issue? Multiple? -->
Closes #
[1-855](https://linear.app/unleash/issue/1-855/allow-for-title-on-strategy-backend)

<!-- (For internal contributors): Does it relate to an issue on public
roadmap? -->
<!--
Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#
-->

### Important files
<!-- PRs can contain a lot of changes, but not all changes are equally
important. Where should a reviewer start looking to get an overview of
the changes? Are any files particularly important? -->


## Discussion points
<!-- Anything about the PR you'd like to discuss before it gets merged?
Got any questions or doubts? -->

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
andreas-unleash 2023-04-18 09:59:02 +03:00 committed by GitHub
parent 5940a81158
commit 2da279b7fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 354 additions and 36 deletions

View File

@ -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<IFeatureToggleClient>,
row: Record<string, any>,
) {
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;

View File

@ -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();
}

View File

@ -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',
},

View File

@ -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',
},

View File

@ -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();
});

View File

@ -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,
},
},
},

View File

@ -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) {

View File

@ -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(),

View File

@ -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 {

View File

@ -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<IStrategy, string> {

View File

@ -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({

View File

@ -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,
);
};

View File

@ -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);
});

View File

@ -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",

View File

@ -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,