From 1e3c6901850e1719f870dae08350ef806564e038 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Thu, 25 Jul 2024 13:36:28 +0300 Subject: [PATCH] feat: tag feature on creation (#7664) Now it is possible to tag feature on creation. --- .../feature-toggle/feature-toggle-service.ts | 14 ++++++++++-- .../spec/create-feature-schema.test.ts | 5 ++++- src/lib/openapi/spec/create-feature-schema.ts | 12 ++++++---- src/lib/openapi/spec/export-result-schema.ts | 2 ++ src/lib/openapi/spec/features-schema.ts | 2 ++ src/lib/schema/feature-schema.ts | 9 ++++++++ src/lib/types/model.ts | 1 + src/test/e2e/api/client/feature.e2e.test.ts | 22 +++++++++++++++++++ 8 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/lib/features/feature-toggle/feature-toggle-service.ts b/src/lib/features/feature-toggle/feature-toggle-service.ts index c772db4f5c..08a4fa3ad8 100644 --- a/src/lib/features/feature-toggle/feature-toggle-service.ts +++ b/src/lib/features/feature-toggle/feature-toggle-service.ts @@ -1256,7 +1256,7 @@ class FeatureToggleService { await this.validateName(value.name); await this.validateFeatureFlagNameAgainstPattern(value.name, projectId); - const exists = await this.projectStore.hasProject(projectId); + const projectExists = await this.projectStore.hasProject(projectId); if (await this.projectStore.isFeatureLimitReached(projectId)) { throw new InvalidOperationError( @@ -1266,7 +1266,7 @@ class FeatureToggleService { await this.validateFeatureFlagLimit(); - if (exists) { + if (projectExists) { let featureData: FeatureToggleInsert; if (isValidated) { featureData = { createdByUserId: auditUser.id, ...value }; @@ -1301,6 +1301,16 @@ class FeatureToggleService { ); } + if (value.tags && value.tags.length > 0) { + const mapTagsToFeatureTagInserts = value.tags.map((tag) => ({ + tagValue: tag.value, + tagType: tag.type, + createdByUserId: auditUser.id, + featureName: featureName, + })); + await this.tagStore.tagFeatures(mapTagsToFeatureTagInserts); + } + await this.eventService.storeEvent( new FeatureCreatedEvent({ featureName, diff --git a/src/lib/openapi/spec/create-feature-schema.test.ts b/src/lib/openapi/spec/create-feature-schema.test.ts index 665f6ef3e0..588de99ca2 100644 --- a/src/lib/openapi/spec/create-feature-schema.test.ts +++ b/src/lib/openapi/spec/create-feature-schema.test.ts @@ -8,7 +8,10 @@ test('createFeatureSchema', () => { description: 'Controls disabling of the comments section in case of an incident', impressionData: false, - tags: ['simple:test', 'simple:test2'], + tags: [ + { type: 'simple', value: 'tag' }, + { type: 'simple', value: 'mytag' }, + ], }; expect( diff --git a/src/lib/openapi/spec/create-feature-schema.ts b/src/lib/openapi/spec/create-feature-schema.ts index 62e09c98f1..74a92a4460 100644 --- a/src/lib/openapi/spec/create-feature-schema.ts +++ b/src/lib/openapi/spec/create-feature-schema.ts @@ -1,4 +1,5 @@ import type { FromSchema } from 'json-schema-to-ts'; +import { tagSchema } from './tag-schema'; export const createFeatureSchema = { $id: '#/components/schemas/createFeatureSchema', @@ -32,14 +33,17 @@ export const createFeatureSchema = { }, tags: { type: 'array', + description: 'Tags to add to the feature.', items: { - type: 'string', - example: 'simple:test', + $ref: '#/components/schemas/tagSchema', }, - description: 'List of tags associated with the feature', }, }, - components: {}, + components: { + schemas: { + tagSchema, + }, + }, } as const; export type CreateFeatureSchema = FromSchema; diff --git a/src/lib/openapi/spec/export-result-schema.ts b/src/lib/openapi/spec/export-result-schema.ts index e1510fbe2d..710e8989f9 100644 --- a/src/lib/openapi/spec/export-result-schema.ts +++ b/src/lib/openapi/spec/export-result-schema.ts @@ -14,6 +14,7 @@ import { tagTypeSchema } from './tag-type-schema'; import { strategyVariantSchema } from './strategy-variant-schema'; import { featureDependenciesSchema } from './feature-dependencies-schema'; import { dependentFeatureSchema } from './dependent-feature-schema'; +import { tagSchema } from './tag-schema'; export const exportResultSchema = { $id: '#/components/schemas/exportResultSchema', @@ -194,6 +195,7 @@ export const exportResultSchema = { tagTypeSchema, featureDependenciesSchema, dependentFeatureSchema, + tagSchema, }, }, } as const; diff --git a/src/lib/openapi/spec/features-schema.ts b/src/lib/openapi/spec/features-schema.ts index 7bc813c85f..afaadd2d3e 100644 --- a/src/lib/openapi/spec/features-schema.ts +++ b/src/lib/openapi/spec/features-schema.ts @@ -8,6 +8,7 @@ import { featureStrategySchema } from './feature-strategy-schema'; import { environmentSchema } from './environment-schema'; import { featureEnvironmentSchema } from './feature-environment-schema'; import { strategyVariantSchema } from './strategy-variant-schema'; +import { tagSchema } from './tag-schema'; export const featuresSchema = { $id: '#/components/schemas/featuresSchema', @@ -40,6 +41,7 @@ export const featuresSchema = { strategyVariantSchema, parametersSchema, variantSchema, + tagSchema, }, }, } as const; diff --git a/src/lib/schema/feature-schema.ts b/src/lib/schema/feature-schema.ts index dfdd98efd1..2ee98cd73c 100644 --- a/src/lib/schema/feature-schema.ts +++ b/src/lib/schema/feature-schema.ts @@ -104,6 +104,15 @@ export const featureMetadataSchema = joi .unique((a, b) => a.name === b.name) .optional() .items(variantsSchema), + tags: joi + .array() + .optional() + .items( + joi.object().keys({ + type: joi.string().required(), + value: joi.string().required(), + }), + ), createdByUserId: joi.number(), }) .options({ allowUnknown: false, stripUnknown: true, abortEarly: false }); diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 1b21994a3a..fa11ff4c24 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -63,6 +63,7 @@ export interface FeatureToggleDTO { createdAt?: Date; impressionData?: boolean; variants?: IVariant[]; + tags?: ITag[]; createdByUserId?: number; createdBy?: { id: number; diff --git a/src/test/e2e/api/client/feature.e2e.test.ts b/src/test/e2e/api/client/feature.e2e.test.ts index 114d93ca7c..38bd9aec8c 100644 --- a/src/test/e2e/api/client/feature.e2e.test.ts +++ b/src/test/e2e/api/client/feature.e2e.test.ts @@ -358,3 +358,25 @@ test('returns a feature flags impression data for a different project', async () expect(projectFlag.impressionData).toBe(true); }); }); + +test('Can add tags while creating feature flag', async () => { + const featureName = 'test.feature.with.tagss'; + const tags = [{ value: 'tag1', type: 'simple' }]; + + await app.request.post('/api/admin/tags').send(tags[0]); + + await app.request.post('/api/admin/projects/default/features').send({ + name: featureName, + type: 'killswitch', + tags, + }); + + const { body } = await app.request + .get(`/api/admin/features/${featureName}/tags`) + .expect('Content-Type', /json/) + .expect(200); + + expect(body).toMatchObject({ + tags, + }); +});