mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-04 00:18:40 +01:00
feat: implement column created_by_user_id in feature_tag (#5695)
## About the changes Adds the new nullable column created_by_user_id to the data used by feature-tag-store and feature-tag-service. Also updates openapi schemas.
This commit is contained in:
parent
e0f83347ab
commit
4e56d1d8d5
@ -24,4 +24,6 @@ export interface FeatureTagSchema {
|
||||
* @deprecated
|
||||
*/
|
||||
value?: string;
|
||||
/** The id of the user who created this tag */
|
||||
createdByUserId?: number;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { DB_TIME } from '../metric-events';
|
||||
import {
|
||||
IFeatureAndTag,
|
||||
IFeatureTag,
|
||||
IFeatureTagInsert,
|
||||
IFeatureTagStore,
|
||||
} from '../types/stores/feature-tag-store';
|
||||
import { Db } from './db';
|
||||
@ -18,6 +19,7 @@ interface FeatureTagTable {
|
||||
feature_name: string;
|
||||
tag_type: string;
|
||||
tag_value: string;
|
||||
created_by_user_id?: number;
|
||||
}
|
||||
|
||||
class FeatureTagStore implements IFeatureTagStore {
|
||||
@ -82,6 +84,7 @@ class FeatureTagStore implements IFeatureTagStore {
|
||||
featureName: row.feature_name,
|
||||
tagType: row.tag_type,
|
||||
tagValue: row.tag_value,
|
||||
createdByUserId: row.created_by_user_id,
|
||||
};
|
||||
}
|
||||
|
||||
@ -91,6 +94,7 @@ class FeatureTagStore implements IFeatureTagStore {
|
||||
featureName: row.feature_name,
|
||||
tagType: row.tag_type,
|
||||
tagValue: row.tag_value,
|
||||
createdByUserId: row.created_by_user_id,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -138,13 +142,18 @@ class FeatureTagStore implements IFeatureTagStore {
|
||||
featureName: row.feature_name,
|
||||
tagType: row.tag_type,
|
||||
tagValue: row.tag_value,
|
||||
createdByUserId: row.created_by_user_id,
|
||||
}));
|
||||
}
|
||||
|
||||
async tagFeature(featureName: string, tag: ITag): Promise<ITag> {
|
||||
async tagFeature(
|
||||
featureName: string,
|
||||
tag: ITag,
|
||||
createdByUserId: number,
|
||||
): Promise<ITag> {
|
||||
const stopTimer = this.timer('tagFeature');
|
||||
await this.db<FeatureTagTable>(TABLE)
|
||||
.insert(this.featureAndTagToRow(featureName, tag))
|
||||
.insert(this.featureAndTagToRow(featureName, tag, createdByUserId))
|
||||
.onConflict(COLUMNS)
|
||||
.merge();
|
||||
stopTimer();
|
||||
@ -177,6 +186,7 @@ class FeatureTagStore implements IFeatureTagStore {
|
||||
featureName: row.feature_name,
|
||||
tagType: row.tag_type,
|
||||
tagValue: row.tag_value,
|
||||
createdByUserId: row.created_by_user_id,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -186,7 +196,9 @@ class FeatureTagStore implements IFeatureTagStore {
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
async tagFeatures(featureTags: IFeatureTag[]): Promise<IFeatureAndTag[]> {
|
||||
async tagFeatures(
|
||||
featureTags: IFeatureTagInsert[],
|
||||
): Promise<IFeatureAndTag[]> {
|
||||
if (featureTags.length !== 0) {
|
||||
const rows = await this.db(TABLE)
|
||||
.insert(featureTags.map(this.featureTagToRow))
|
||||
@ -204,7 +216,11 @@ class FeatureTagStore implements IFeatureTagStore {
|
||||
const stopTimer = this.timer('untagFeature');
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
.where(this.featureAndTagToRow(featureName, tag))
|
||||
.where({
|
||||
feature_name: featureName,
|
||||
tag_type: tag.type,
|
||||
tag_value: tag.value,
|
||||
})
|
||||
.delete();
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
@ -233,11 +249,13 @@ class FeatureTagStore implements IFeatureTagStore {
|
||||
featureName,
|
||||
tagType,
|
||||
tagValue,
|
||||
}: IFeatureTag): FeatureTagTable {
|
||||
createdByUserId,
|
||||
}: IFeatureTagInsert): FeatureTagTable {
|
||||
return {
|
||||
feature_name: featureName,
|
||||
tag_type: tagType,
|
||||
tag_value: tagValue,
|
||||
created_by_user_id: createdByUserId,
|
||||
};
|
||||
}
|
||||
|
||||
@ -248,11 +266,13 @@ class FeatureTagStore implements IFeatureTagStore {
|
||||
featureAndTagToRow(
|
||||
featureName: string,
|
||||
{ type, value }: ITag,
|
||||
createdByUserId: number,
|
||||
): FeatureTagTable {
|
||||
return {
|
||||
feature_name: featureName,
|
||||
tag_type: type,
|
||||
tag_value: value,
|
||||
created_by_user_id: createdByUserId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
const sortOrderFirst = 0;
|
||||
const sortOrderSecond = 10;
|
||||
const TESTUSERID = 3333;
|
||||
|
||||
const createSegment = async (segmentName: string) => {
|
||||
const segment = await app.services.segmentService.create(
|
||||
@ -2991,7 +2992,7 @@ test('Can filter based on tags', async () => {
|
||||
await db.stores.featureToggleStore.create('default', {
|
||||
name: 'not-tagged',
|
||||
});
|
||||
await db.stores.featureTagStore.tagFeature('to-be-tagged', tag);
|
||||
await db.stores.featureTagStore.tagFeature('to-be-tagged', tag, TESTUSERID);
|
||||
await app.request
|
||||
.get('/api/admin/projects/default/features?tag=simple:hello-tags')
|
||||
.expect((res) => {
|
||||
@ -3028,10 +3029,12 @@ test('Can query for features with namePrefix and tags', async () => {
|
||||
await db.stores.featureTagStore.tagFeature(
|
||||
'to-be-tagged-nameprefix-and-tags',
|
||||
tag,
|
||||
TESTUSERID,
|
||||
);
|
||||
await db.stores.featureTagStore.tagFeature(
|
||||
'tagged-but-not-hit-nameprefix-and-tags',
|
||||
tag,
|
||||
TESTUSERID,
|
||||
);
|
||||
await app.request
|
||||
.get(
|
||||
@ -3065,13 +3068,26 @@ test('Can query for two tags at the same time. Tags are ORed together', async ()
|
||||
name: 'tagged-with-both-tags',
|
||||
},
|
||||
);
|
||||
await db.stores.featureTagStore.tagFeature(taggedWithFirst.name, tag);
|
||||
await db.stores.featureTagStore.tagFeature(
|
||||
taggedWithFirst.name,
|
||||
tag,
|
||||
TESTUSERID,
|
||||
);
|
||||
await db.stores.featureTagStore.tagFeature(
|
||||
taggedWithSecond.name,
|
||||
secondTag,
|
||||
TESTUSERID,
|
||||
);
|
||||
await db.stores.featureTagStore.tagFeature(
|
||||
taggedWithBoth.name,
|
||||
tag,
|
||||
TESTUSERID,
|
||||
);
|
||||
await db.stores.featureTagStore.tagFeature(
|
||||
taggedWithBoth.name,
|
||||
secondTag,
|
||||
TESTUSERID,
|
||||
);
|
||||
await db.stores.featureTagStore.tagFeature(taggedWithBoth.name, tag);
|
||||
await db.stores.featureTagStore.tagFeature(taggedWithBoth.name, secondTag);
|
||||
await app.request
|
||||
.get(
|
||||
`/api/admin/projects/default/features?tag=${tag.type}:${tag.value}&tag=${secondTag.type}:${secondTag.value}`,
|
||||
|
@ -35,6 +35,12 @@ export const featureTagSchema = {
|
||||
description:
|
||||
'The value of the tag. This property is deprecated and will be removed in a future version of Unleash. Superseded by the `tagValue` property.',
|
||||
},
|
||||
createdByUserId: {
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
example: 1,
|
||||
description: 'The id of the user who created this tag',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
@ -159,4 +159,5 @@ export const featureTagSchema = joi.object().keys({
|
||||
tagValue: joi.string(),
|
||||
type: nameType.optional(),
|
||||
value: joi.string().optional(),
|
||||
createdByUserId: joi.number().optional(),
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ import { IFeatureToggleStore, IUnleashStores } from '../types/stores';
|
||||
import { tagSchema } from './tag-schema';
|
||||
import {
|
||||
IFeatureTag,
|
||||
IFeatureTagInsert,
|
||||
IFeatureTagStore,
|
||||
} from '../types/stores/feature-tag-store';
|
||||
import { ITagStore } from '../types/stores/tag-store';
|
||||
@ -61,7 +62,11 @@ class FeatureTagService {
|
||||
const featureToggle = await this.featureToggleStore.get(featureName);
|
||||
const validatedTag = await tagSchema.validateAsync(tag);
|
||||
await this.createTagIfNeeded(validatedTag, userName, addedByUserId);
|
||||
await this.featureTagStore.tagFeature(featureName, validatedTag);
|
||||
await this.featureTagStore.tagFeature(
|
||||
featureName,
|
||||
validatedTag,
|
||||
addedByUserId,
|
||||
);
|
||||
|
||||
await this.eventService.storeEvent({
|
||||
type: FEATURE_TAGGED,
|
||||
@ -88,19 +93,20 @@ class FeatureTagService {
|
||||
this.createTagIfNeeded(tag, userName, updatedByUserId),
|
||||
),
|
||||
);
|
||||
const createdFeatureTags: IFeatureTag[] = featureNames.flatMap(
|
||||
const createdFeatureTags: IFeatureTagInsert[] = featureNames.flatMap(
|
||||
(featureName) =>
|
||||
addedTags.map((addedTag) => ({
|
||||
featureName,
|
||||
tagType: addedTag.type,
|
||||
tagValue: addedTag.value,
|
||||
createdByUserId: updatedByUserId,
|
||||
})),
|
||||
);
|
||||
|
||||
await this.featureTagStore.tagFeatures(createdFeatureTags);
|
||||
|
||||
const removedFeatureTags: IFeatureTag[] = featureNames.flatMap(
|
||||
(featureName) =>
|
||||
const removedFeatureTags: Omit<IFeatureTag, 'createdByUserId'>[] =
|
||||
featureNames.flatMap((featureName) =>
|
||||
removedTags.map((addedTag) => ({
|
||||
featureName,
|
||||
tagType: addedTag.type,
|
||||
|
@ -16,6 +16,7 @@ import variantsExportV3 from '../../test/examples/variantsexport_v3.json';
|
||||
import EventService from './event-service';
|
||||
import { SYSTEM_USER_ID } from '../types';
|
||||
const oldExportExample = require('./state-service-export-v1.json');
|
||||
const TESTUSERID = 3333;
|
||||
|
||||
function getSetup() {
|
||||
const stores = createStores();
|
||||
@ -398,10 +399,14 @@ test('Should not import an existing tag', async () => {
|
||||
};
|
||||
await stores.tagTypeStore.createTagType(data.tagTypes[0]);
|
||||
await stores.tagStore.createTag(data.tags[0]);
|
||||
await stores.featureTagStore.tagFeature(data.featureTags[0].featureName, {
|
||||
await stores.featureTagStore.tagFeature(
|
||||
data.featureTags[0].featureName,
|
||||
{
|
||||
type: data.featureTags[0].tagType,
|
||||
value: data.featureTags[0].tagValue,
|
||||
});
|
||||
},
|
||||
TESTUSERID,
|
||||
);
|
||||
await stateService.import({
|
||||
data,
|
||||
userId: SYSTEM_USER_ID,
|
||||
@ -466,10 +471,14 @@ test('should export tag, tagtypes but not feature tags if the feature is not exp
|
||||
};
|
||||
await stores.tagTypeStore.createTagType(data.tagTypes[0]);
|
||||
await stores.tagStore.createTag(data.tags[0]);
|
||||
await stores.featureTagStore.tagFeature(data.featureTags[0].featureName, {
|
||||
await stores.featureTagStore.tagFeature(
|
||||
data.featureTags[0].featureName,
|
||||
{
|
||||
type: data.featureTags[0].tagType,
|
||||
value: data.featureTags[0].tagValue,
|
||||
});
|
||||
},
|
||||
TESTUSERID,
|
||||
);
|
||||
|
||||
const exported = await stateService.export({
|
||||
includeFeatureToggles: false,
|
||||
@ -504,10 +513,14 @@ test('should export tag, tagtypes, featureTags and features', async () => {
|
||||
};
|
||||
await stores.tagTypeStore.createTagType(data.tagTypes[0]);
|
||||
await stores.tagStore.createTag(data.tags[0]);
|
||||
await stores.featureTagStore.tagFeature(data.featureTags[0].featureName, {
|
||||
await stores.featureTagStore.tagFeature(
|
||||
data.featureTags[0].featureName,
|
||||
{
|
||||
type: data.featureTags[0].tagType,
|
||||
value: data.featureTags[0].tagValue,
|
||||
});
|
||||
},
|
||||
TESTUSERID,
|
||||
);
|
||||
|
||||
const exported = await stateService.export({
|
||||
includeFeatureToggles: true,
|
||||
@ -667,10 +680,14 @@ test('exporting to new format works', async () => {
|
||||
parameters: {},
|
||||
constraints: [],
|
||||
});
|
||||
await stores.featureTagStore.tagFeature('Some-feature', {
|
||||
await stores.featureTagStore.tagFeature(
|
||||
'Some-feature',
|
||||
{
|
||||
type: 'simple',
|
||||
value: 'Test',
|
||||
});
|
||||
},
|
||||
TESTUSERID,
|
||||
);
|
||||
const exported = await stateService.export({});
|
||||
expect(exported.featureStrategies).toHaveLength(1);
|
||||
});
|
||||
@ -725,10 +742,14 @@ test('featureStrategies can keep existing', async () => {
|
||||
parameters: {},
|
||||
constraints: [],
|
||||
});
|
||||
await stores.featureTagStore.tagFeature('Some-feature', {
|
||||
await stores.featureTagStore.tagFeature(
|
||||
'Some-feature',
|
||||
{
|
||||
type: 'simple',
|
||||
value: 'Test',
|
||||
});
|
||||
},
|
||||
TESTUSERID,
|
||||
);
|
||||
|
||||
const exported = await stateService.export({});
|
||||
await stateService.import({
|
||||
@ -776,10 +797,14 @@ test('featureStrategies should not keep existing if dropBeforeImport', async ()
|
||||
parameters: {},
|
||||
constraints: [],
|
||||
});
|
||||
await stores.featureTagStore.tagFeature('Some-feature', {
|
||||
await stores.featureTagStore.tagFeature(
|
||||
'Some-feature',
|
||||
{
|
||||
type: 'simple',
|
||||
value: 'Test',
|
||||
});
|
||||
},
|
||||
TESTUSERID,
|
||||
);
|
||||
|
||||
const exported = await stateService.export({});
|
||||
exported.featureStrategies = [];
|
||||
|
@ -616,13 +616,18 @@ export default class StateService {
|
||||
userName: string,
|
||||
userId: number,
|
||||
): Promise<void> {
|
||||
const featureTagsToInsert = featureTags.filter((tag) =>
|
||||
const featureTagsToInsert = featureTags
|
||||
.filter((tag) =>
|
||||
keepExisting
|
||||
? !oldFeatureTags.some((old) =>
|
||||
this.compareFeatureTags(old, tag),
|
||||
)
|
||||
: true,
|
||||
);
|
||||
)
|
||||
.map((tag) => ({
|
||||
createdByUserId: userId,
|
||||
...tag,
|
||||
}));
|
||||
if (featureTagsToInsert.length > 0) {
|
||||
const importedFeatureTags =
|
||||
await this.featureTagStore.tagFeatures(featureTagsToInsert);
|
||||
|
@ -5,6 +5,12 @@ export interface IFeatureTag {
|
||||
featureName: string;
|
||||
tagType: string;
|
||||
tagValue: string;
|
||||
createdByUserId?: number;
|
||||
}
|
||||
|
||||
export interface IFeatureTagInsert
|
||||
extends Omit<IFeatureTag, 'created_by_user_id'> {
|
||||
createdByUserId: number;
|
||||
}
|
||||
|
||||
export interface IFeatureAndTag {
|
||||
@ -15,8 +21,14 @@ export interface IFeatureTagStore extends Store<IFeatureTag, IFeatureTag> {
|
||||
getAllTagsForFeature(featureName: string): Promise<ITag[]>;
|
||||
getAllFeaturesForTag(tagValue: string): Promise<string[]>;
|
||||
getAllByFeatures(features: string[]): Promise<IFeatureTag[]>;
|
||||
tagFeature(featureName: string, tag: ITag): Promise<ITag>;
|
||||
tagFeatures(featureTags: IFeatureTag[]): Promise<IFeatureAndTag[]>;
|
||||
tagFeature(
|
||||
featureName: string,
|
||||
tag: ITag,
|
||||
createdByUserId: number,
|
||||
): Promise<ITag>;
|
||||
tagFeatures(featureTags: IFeatureTagInsert[]): Promise<IFeatureAndTag[]>;
|
||||
untagFeature(featureName: string, tag: ITag): Promise<void>;
|
||||
untagFeatures(featureTags: IFeatureTag[]): Promise<void>;
|
||||
untagFeatures(
|
||||
featureTags: Omit<IFeatureTag, 'createdByUserId'>[],
|
||||
): Promise<void>;
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ let featureToggleStore: IFeatureToggleStore;
|
||||
|
||||
const featureName = 'test-tag';
|
||||
const tag = { type: 'simple', value: 'test' };
|
||||
const TESTUSERID = 3333;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('feature_tag_store_serial', getLogger);
|
||||
@ -31,12 +32,13 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
test('should tag feature', async () => {
|
||||
await featureTagStore.tagFeature(featureName, tag);
|
||||
await featureTagStore.tagFeature(featureName, tag, TESTUSERID);
|
||||
const featureTags = await featureTagStore.getAllTagsForFeature(featureName);
|
||||
const featureTag = await featureTagStore.get({
|
||||
featureName,
|
||||
tagType: tag.type,
|
||||
tagValue: tag.value,
|
||||
createdByUserId: TESTUSERID,
|
||||
});
|
||||
expect(featureTags).toHaveLength(1);
|
||||
expect(featureTags[0]).toStrictEqual(tag);
|
||||
@ -45,39 +47,41 @@ test('should tag feature', async () => {
|
||||
});
|
||||
|
||||
test('feature tag exists', async () => {
|
||||
await featureTagStore.tagFeature(featureName, tag);
|
||||
await featureTagStore.tagFeature(featureName, tag, TESTUSERID);
|
||||
const exists = await featureTagStore.exists({
|
||||
featureName,
|
||||
tagType: tag.type,
|
||||
tagValue: tag.value,
|
||||
createdByUserId: TESTUSERID,
|
||||
});
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
||||
test('should delete feature tag', async () => {
|
||||
await featureTagStore.tagFeature(featureName, tag);
|
||||
await featureTagStore.tagFeature(featureName, tag, TESTUSERID);
|
||||
await featureTagStore.delete({
|
||||
featureName,
|
||||
tagType: tag.type,
|
||||
tagValue: tag.value,
|
||||
createdByUserId: TESTUSERID,
|
||||
});
|
||||
const featureTags = await featureTagStore.getAllTagsForFeature(featureName);
|
||||
expect(featureTags).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should untag feature', async () => {
|
||||
await featureTagStore.tagFeature(featureName, tag);
|
||||
await featureTagStore.tagFeature(featureName, tag, TESTUSERID);
|
||||
await featureTagStore.untagFeature(featureName, tag);
|
||||
const featureTags = await featureTagStore.getAllTagsForFeature(featureName);
|
||||
expect(featureTags).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('get all feature tags', async () => {
|
||||
await featureTagStore.tagFeature(featureName, tag);
|
||||
await featureTagStore.tagFeature(featureName, tag, TESTUSERID);
|
||||
await featureToggleStore.create('default', {
|
||||
name: 'some-other-toggle',
|
||||
});
|
||||
await featureTagStore.tagFeature('some-other-toggle', tag);
|
||||
await featureTagStore.tagFeature('some-other-toggle', tag, TESTUSERID);
|
||||
const all = await featureTagStore.getAll();
|
||||
expect(all).toHaveLength(2);
|
||||
});
|
||||
@ -87,11 +91,17 @@ test('should import feature tags', async () => {
|
||||
name: 'some-other-toggle-import',
|
||||
});
|
||||
await featureTagStore.tagFeatures([
|
||||
{ featureName, tagType: tag.type, tagValue: tag.value },
|
||||
{
|
||||
featureName,
|
||||
tagType: tag.type,
|
||||
tagValue: tag.value,
|
||||
createdByUserId: TESTUSERID,
|
||||
},
|
||||
{
|
||||
featureName: 'some-other-toggle-import',
|
||||
tagType: tag.type,
|
||||
tagValue: tag.value,
|
||||
createdByUserId: TESTUSERID,
|
||||
},
|
||||
]);
|
||||
|
||||
|
15
src/test/fixtures/fake-feature-tag-store.ts
vendored
15
src/test/fixtures/fake-feature-tag-store.ts
vendored
@ -46,11 +46,16 @@ export default class FakeFeatureTagStore implements IFeatureTagStore {
|
||||
return this.featureTags;
|
||||
}
|
||||
|
||||
async tagFeature(featureName: string, tag: ITag): Promise<ITag> {
|
||||
async tagFeature(
|
||||
featureName: string,
|
||||
tag: ITag,
|
||||
createdByUserId: number,
|
||||
): Promise<ITag> {
|
||||
this.featureTags.push({
|
||||
featureName,
|
||||
tagType: tag.type,
|
||||
tagValue: tag.value,
|
||||
createdByUserId,
|
||||
});
|
||||
return Promise.resolve(tag);
|
||||
}
|
||||
@ -67,10 +72,14 @@ export default class FakeFeatureTagStore implements IFeatureTagStore {
|
||||
async tagFeatures(featureTags: IFeatureTag[]): Promise<IFeatureAndTag[]> {
|
||||
return Promise.all(
|
||||
featureTags.map(async (fT) => {
|
||||
const saved = await this.tagFeature(fT.featureName, {
|
||||
const saved = await this.tagFeature(
|
||||
fT.featureName,
|
||||
{
|
||||
value: fT.tagValue,
|
||||
type: fT.tagType,
|
||||
});
|
||||
},
|
||||
fT.createdByUserId,
|
||||
);
|
||||
return {
|
||||
featureName: fT.featureName,
|
||||
tag: saved,
|
||||
|
Loading…
Reference in New Issue
Block a user