diff --git a/src/lib/db/feature-strategy-store.test.ts b/src/lib/db/feature-strategy-store.test.ts index b52332b3ffba..cabee6bc8bf3 100644 --- a/src/lib/db/feature-strategy-store.test.ts +++ b/src/lib/db/feature-strategy-store.test.ts @@ -41,6 +41,7 @@ test('returns 0 if no custom strategies are in use', async () => { featureToggleStore.create('default', { name: 'test-toggle-2', + createdByUserId: 9999, }); strategyStore.createStrategy({ @@ -68,6 +69,7 @@ test('counts custom strategies in use', async () => { await featureToggleStore.create('default', { name: 'test-toggle', + createdByUserId: 9999, }); await strategyStore.createStrategy({ @@ -112,6 +114,7 @@ test('increment sort order on each new insert', async () => { await featureToggleStore.create('default', { name: 'test-toggle-increment', + createdByUserId: 9999, }); const { id: firstId } = diff --git a/src/lib/features/change-request-segment-usage-service/change-request-segment-usage-read-model.test.ts b/src/lib/features/change-request-segment-usage-service/change-request-segment-usage-read-model.test.ts index af7d77276920..41f039e54a7a 100644 --- a/src/lib/features/change-request-segment-usage-service/change-request-segment-usage-read-model.test.ts +++ b/src/lib/features/change-request-segment-usage-service/change-request-segment-usage-read-model.test.ts @@ -28,6 +28,7 @@ beforeAll(async () => { await db.stores.featureToggleStore.create('default', { name: FLAG_NAME, + createdByUserId: 9999, }); }); diff --git a/src/lib/features/feature-search/feature.search.e2e.test.ts b/src/lib/features/feature-search/feature.search.e2e.test.ts index 4fa9d619f6aa..b7b241105418 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -661,6 +661,7 @@ test('should search features by project with operators', async () => { await db.stores.featureToggleStore.create('project_b', { name: 'my_feature_b', + createdByUserId: 9999, }); await db.stores.projectStore.create({ @@ -671,6 +672,7 @@ test('should search features by project with operators', async () => { await db.stores.featureToggleStore.create('project_c', { name: 'my_feature_c', + createdByUserId: 9999, }); const { body } = await searchFeatures({ diff --git a/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts b/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts index 5ef9b9192757..31251373b0d4 100644 --- a/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts +++ b/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts @@ -12,7 +12,10 @@ import { IVariant, } from 'lib/types/model'; import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service'; -import { EnvironmentFeatureNames } from '../feature-toggle-store'; +import { + EnvironmentFeatureNames, + FeatureToggleInsert, +} from '../feature-toggle-store'; import { FeatureConfigurationClient } from '../types/feature-toggle-strategies-store-type'; import { IFeatureProjectUserParams } from '../feature-toggle-controller'; @@ -104,7 +107,10 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { }; } - async create(project: string, data: FeatureToggle): Promise { + async create( + project: string, + data: FeatureToggleInsert, + ): Promise { const inserted: FeatureToggle = { ...data, project }; this.features.push(inserted); return inserted; diff --git a/src/lib/features/feature-toggle/feature-toggle-service.ts b/src/lib/features/feature-toggle/feature-toggle-service.ts index 8f67997d7656..4ff4a33f403c 100644 --- a/src/lib/features/feature-toggle/feature-toggle-service.ts +++ b/src/lib/features/feature-toggle/feature-toggle-service.ts @@ -1150,9 +1150,14 @@ class FeatureToggleService { if (exists) { let featureData; if (isValidated) { - featureData = value; + featureData = { createdByUserId, ...value }; } else { - featureData = await featureMetadataSchema.validateAsync(value); + const validated = + await featureMetadataSchema.validateAsync(value); + featureData = { + createdByUserId, + ...validated, + }; } const featureName = featureData.name; const createdToggle = await this.featureToggleStore.create( diff --git a/src/lib/features/feature-toggle/feature-toggle-store.ts b/src/lib/features/feature-toggle/feature-toggle-store.ts index 7d841b78dcee..f325a09d48e7 100644 --- a/src/lib/features/feature-toggle/feature-toggle-store.ts +++ b/src/lib/features/feature-toggle/feature-toggle-store.ts @@ -49,6 +49,12 @@ export interface FeaturesTable { impression_data: boolean; archived?: boolean; archived_at?: Date; + created_by_user_id?: number; +} + +export interface FeatureToggleInsert + extends Omit { + createdByUserId: number; } interface VariantDTO { @@ -457,7 +463,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { return sortedVariants; } - dtoToRow(project: string, data: FeatureToggleDTO): FeaturesTable { + insertToRow(project: string, data: FeatureToggleInsert): FeaturesTable { const row = { name: data.name, description: data.description, @@ -467,20 +473,39 @@ export default class FeatureToggleStore implements IFeatureToggleStore { stale: data.stale, created_at: data.createdAt, impression_data: data.impressionData, + created_by_user_id: data.createdByUserId, }; if (!row.created_at) { delete row.created_at; } + return row; } - async create( + dtoToUpdateRow( project: string, data: FeatureToggleDTO, + ): Omit { + const row = { + name: data.name, + description: data.description, + type: data.type, + project, + archived_at: data.archived ? new Date() : null, + stale: data.stale, + impression_data: data.impressionData, + }; + + return row; + } + + async create( + project: string, + data: FeatureToggleInsert, ): Promise { try { const row = await this.db(TABLE) - .insert(this.dtoToRow(project, data)) + .insert(this.insertToRow(project, data)) .returning(FEATURE_COLUMNS); return this.rowToFeature(row[0]); @@ -504,7 +529,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { ): Promise { const row = await this.db(TABLE) .where({ name: data.name }) - .update(this.dtoToRow(project, data)) + .update(this.dtoToUpdateRow(project, data)) .returning(FEATURE_COLUMNS); return this.rowToFeature(row[0]); diff --git a/src/lib/features/feature-toggle/tests/archive-feature-toggles.e2e.test.ts b/src/lib/features/feature-toggle/tests/archive-feature-toggles.e2e.test.ts index 4b3c0d15fdc8..e82846245af8 100644 --- a/src/lib/features/feature-toggle/tests/archive-feature-toggles.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/archive-feature-toggles.e2e.test.ts @@ -100,14 +100,17 @@ test('Should get archived toggles via project', async () => { await db.stores.featureToggleStore.create('proj-1', { name: 'feat-proj-1', archived: true, + createdByUserId: 9999, }); await db.stores.featureToggleStore.create('proj-2', { name: 'feat-proj-2', archived: true, + createdByUserId: 9999, }); await db.stores.featureToggleStore.create('proj-2', { name: 'feat-proj-2-2', archived: true, + createdByUserId: 9999, }); await app.request @@ -151,6 +154,7 @@ test('Should disable all environments when reviving a toggle', async () => { await db.stores.featureToggleStore.create('default', { name: 'feat-proj-1', archived: true, + createdByUserId: 9999, }); await db.stores.environmentStore.create({ diff --git a/src/lib/features/feature-toggle/tests/feature-toggle-store.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggle-store.e2e.test.ts index 1f3f43279c9d..e5f3a168578f 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggle-store.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggle-store.e2e.test.ts @@ -5,6 +5,7 @@ import { IFeatureToggleStore, IProjectStore, } from '../../../types'; +import { FeatureToggleInsert } from '../feature-toggle-store'; let stores; let db; @@ -49,10 +50,11 @@ describe('potentially_stale marking', () => { }; test('it returns an empty list if no toggles were updated', async () => { - const features: FeatureToggleDTO[] = [ + const features: FeatureToggleInsert[] = [ { name: 'feature1', type: 'release', + createdByUserId: 9999, }, ]; await Promise.all( @@ -68,14 +70,16 @@ describe('potentially_stale marking', () => { }); test('it returns only updated toggles', async () => { - const features: FeatureToggleDTO[] = [ + const features: FeatureToggleInsert[] = [ { name: 'feature1', type: 'release', + createdByUserId: 9999, }, { name: 'feature2', type: 'kill-switch', + createdByUserId: 9999, }, ]; await Promise.all( @@ -102,13 +106,13 @@ describe('potentially_stale marking', () => { ])( 'it marks toggles based on their type (days elapsed: %s)', async (daysElapsed, expectedMarkedFeatures) => { - const features: FeatureToggleDTO[] = [ + const features: FeatureToggleInsert[] = [ 'release', 'experiment', 'operational', 'kill-switch', 'permission', - ].map((type) => ({ name: type, type })); + ].map((type) => ({ name: type, type, createdByUserId: 9999 })); await Promise.all( features.map((feature) => featureToggleStore.create('default', feature), @@ -143,11 +147,12 @@ describe('potentially_stale marking', () => { }, ); test('it does not mark toggles already flagged as stale', async () => { - const features: FeatureToggleDTO[] = [ + const features: FeatureToggleInsert[] = [ { name: 'feature1', type: 'release', stale: true, + createdByUserId: 9999, }, ]; await Promise.all( @@ -163,10 +168,11 @@ describe('potentially_stale marking', () => { }); test('it does not return toggles previously marked as potentially_stale', async () => { - const features: FeatureToggleDTO[] = [ + const features: FeatureToggleInsert[] = [ { name: 'feature1', type: 'release', + createdByUserId: 9999, }, ]; await Promise.all( @@ -197,10 +203,11 @@ describe('potentially_stale marking', () => { describe('changing feature types', () => { test("if a potentially stale feature changes to a type that shouldn't be stale, it's 'potentially_stale' marker is removed.", async () => { - const features: FeatureToggleDTO[] = [ + const features: FeatureToggleInsert[] = [ { name: 'feature1', type: 'release', + createdByUserId: 9999, }, ]; await Promise.all( @@ -247,10 +254,11 @@ describe('potentially_stale marking', () => { }); test('if a fresh feature changes to a type that should be stale, it gets marked as potentially stale', async () => { - const features: FeatureToggleDTO[] = [ + const features: FeatureToggleInsert[] = [ { name: 'feature1', type: 'kill-switch', + createdByUserId: 9999, }, ]; await Promise.all( @@ -280,11 +288,12 @@ describe('potentially_stale marking', () => { }); test('if a stale feature changes to a type that should be stale, it does not get marked as potentially stale', async () => { - const features: FeatureToggleDTO[] = [ + const features: FeatureToggleInsert[] = [ { name: 'feature1', type: 'kill-switch', stale: true, + createdByUserId: 9999, }, ]; await Promise.all( @@ -314,9 +323,15 @@ describe('potentially_stale marking', () => { name: 'MyProject', description: 'MyProject', }); - await featureToggleStore.create('default', { name: 'featureA' }); + await featureToggleStore.create('default', { + name: 'featureA', + createdByUserId: 9999, + }); - await featureToggleStore.create('MyProject', { name: 'featureB' }); + await featureToggleStore.create('MyProject', { + name: 'featureB', + createdByUserId: 9999, + }); const playgroundFeatures = await featureToggleStore.getPlaygroundFeatures({ diff --git a/src/lib/features/feature-toggle/tests/feature-toggle-strategies-store.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggle-strategies-store.e2e.test.ts index 7deb1d9785da..0822cc11d270 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggle-strategies-store.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggle-strategies-store.e2e.test.ts @@ -15,7 +15,10 @@ beforeAll(async () => { stores = db.stores; featureStrategiesStore = stores.featureStrategiesStore; featureToggleStore = stores.featureToggleStore; - await featureToggleStore.create('default', { name: featureName }); + await featureToggleStore.create('default', { + name: featureName, + createdByUserId: 9999, + }); }); afterAll(async () => { @@ -74,8 +77,14 @@ test('Can successfully update project for all strategies belonging to feature', test('Can query for features with tags', async () => { const tag = { type: 'simple', value: 'hello-tags' }; await stores.tagStore.createTag(tag); - await featureToggleStore.create('default', { name: 'to-be-tagged' }); - await featureToggleStore.create('default', { name: 'not-tagged' }); + await featureToggleStore.create('default', { + name: 'to-be-tagged', + createdByUserId: 9999, + }); + await featureToggleStore.create('default', { + name: 'not-tagged', + createdByUserId: 9999, + }); await stores.featureTagStore.tagFeature('to-be-tagged', tag); const features = await featureStrategiesStore.getFeatureOverview({ projectId: 'default', @@ -87,9 +96,11 @@ test('Can query for features with tags', async () => { test('Can query for features with namePrefix', async () => { await featureToggleStore.create('default', { name: 'nameprefix-to-be-hit', + createdByUserId: 9999, }); await featureToggleStore.create('default', { name: 'nameprefix-not-be-hit', + createdByUserId: 9999, }); const features = await featureStrategiesStore.getFeatureOverview({ projectId: 'default', @@ -103,12 +114,15 @@ test('Can query for features with namePrefix and tags', async () => { await stores.tagStore.createTag(tag); await featureToggleStore.create('default', { name: 'to-be-tagged-nameprefix-and-tags', + createdByUserId: 9999, }); await featureToggleStore.create('default', { name: 'not-tagged-nameprefix-and-tags', + createdByUserId: 9999, }); await featureToggleStore.create('default', { name: 'tagged-but-not-hit-nameprefix-and-tags', + createdByUserId: 9999, }); await stores.featureTagStore.tagFeature( 'to-be-tagged-nameprefix-and-tags', diff --git a/src/lib/features/feature-toggle/tests/feature-toggles.auth.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggles.auth.e2e.test.ts index 5e9bc5fa768f..4cf6bffc2360 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggles.auth.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggles.auth.e2e.test.ts @@ -40,7 +40,10 @@ test('Should not be possible to update feature toggle without permission', async const url = '/api/admin/projects/default/features'; const name = 'auth.toggle.update'; - await db.stores.featureToggleStore.create('default', { name }); + await db.stores.featureToggleStore.create('default', { + name, + createdByUserId: 9999, + }); await app.services.userService.createUser({ email, @@ -62,7 +65,10 @@ test('Should be possible to update feature toggle with permission', async () => const url = '/api/admin/projects/default/features'; const name = 'auth.toggle.update2'; - await db.stores.featureToggleStore.create('default', { name }); + await db.stores.featureToggleStore.create('default', { + name, + createdByUserId: 9999, + }); await app.services.userService.createUser({ email, diff --git a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts index 1ab6430c914c..fc1040ddb675 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts @@ -2574,6 +2574,7 @@ test('should reject invalid constraint values for multi-valued constraints', asy const toggle = await db.stores.featureToggleStore.create(project.id, { name: uuidv4(), impressionData: true, + createdByUserId: 9999, }); const mockStrategy = (values: string[]) => ({ @@ -2621,6 +2622,7 @@ test('should add default constraint values for single-valued constraints', async const toggle = await db.stores.featureToggleStore.create(project.id, { name: uuidv4(), impressionData: true, + createdByUserId: 9999, }); const constraintValue = { @@ -2680,6 +2682,7 @@ test('should allow long parameter values', async () => { const toggle = await db.stores.featureToggleStore.create(project.id, { name: uuidv4(), + createdByUserId: 9999, }); const strategy = { @@ -2988,9 +2991,11 @@ test('Can filter based on tags', async () => { await db.stores.tagStore.createTag(tag); await db.stores.featureToggleStore.create('default', { name: 'to-be-tagged', + createdByUserId: 9999, }); await db.stores.featureToggleStore.create('default', { name: 'not-tagged', + createdByUserId: 9999, }); await db.stores.featureTagStore.tagFeature('to-be-tagged', tag, TESTUSERID); await app.request @@ -3003,9 +3008,11 @@ test('Can filter based on tags', async () => { test('Can query for features with namePrefix', async () => { await db.stores.featureToggleStore.create('default', { name: 'nameprefix-to-be-hit', + createdByUserId: 9999, }); await db.stores.featureToggleStore.create('default', { name: 'nameprefix-not-be-hit', + createdByUserId: 9999, }); await app.request .get('/api/admin/projects/default/features?namePrefix=nameprefix-to') @@ -3019,12 +3026,15 @@ test('Can query for features with namePrefix and tags', async () => { await db.stores.tagStore.createTag(tag); await db.stores.featureToggleStore.create('default', { name: 'to-be-tagged-nameprefix-and-tags', + createdByUserId: 9999, }); await db.stores.featureToggleStore.create('default', { name: 'not-tagged-nameprefix-and-tags', + createdByUserId: 9999, }); await db.stores.featureToggleStore.create('default', { name: 'tagged-but-not-hit-nameprefix-and-tags', + createdByUserId: 9999, }); await db.stores.featureTagStore.tagFeature( 'to-be-tagged-nameprefix-and-tags', @@ -3054,18 +3064,21 @@ test('Can query for two tags at the same time. Tags are ORed together', async () 'default', { name: 'tagged-with-first-tag', + createdByUserId: 9999, }, ); const taggedWithSecond = await db.stores.featureToggleStore.create( 'default', { name: 'tagged-with-second-tag', + createdByUserId: 9999, }, ); const taggedWithBoth = await db.stores.featureToggleStore.create( 'default', { name: 'tagged-with-both-tags', + createdByUserId: 9999, }, ); await db.stores.featureTagStore.tagFeature( diff --git a/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts b/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts index 32c42fbd86b9..e76359505870 100644 --- a/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts +++ b/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts @@ -5,6 +5,7 @@ import { IFeatureTypeCount, IVariant, } from '../../../types/model'; +import { FeatureToggleInsert } from '../feature-toggle-store'; import { Store } from '../../../types/stores/store'; import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service'; import { FeatureConfigurationClient } from './feature-toggle-strategies-store-type'; @@ -24,7 +25,7 @@ export interface IFeatureToggleStore extends Store { getProjectId(name: string): Promise; - create(project: string, data: FeatureToggleDTO): Promise; + create(project: string, data: FeatureToggleInsert): Promise; update(project: string, data: FeatureToggleDTO): Promise; diff --git a/src/lib/features/project-environments/environment-service.test.ts b/src/lib/features/project-environments/environment-service.test.ts index 50cd32f8e7ae..1c5d4be5fff1 100644 --- a/src/lib/features/project-environments/environment-service.test.ts +++ b/src/lib/features/project-environments/environment-service.test.ts @@ -52,6 +52,7 @@ test('Can connect environment to project', async () => { type: 'release', description: '', stale: false, + createdByUserId: 9999, }); await service.addEnvironmentToProject( 'test-connection', @@ -93,6 +94,7 @@ test('Can remove environment from project', async () => { }); await stores.featureToggleStore.create('default', { name: 'removal-test', + createdByUserId: 9999, }); await service.removeEnvironmentFromProject( 'test-connection', @@ -285,6 +287,7 @@ test('When given overrides should remap projects to override environments', asyn type: 'release', description: '', stale: false, + createdByUserId: 9999, }); await service.addEnvironmentToProject( diff --git a/src/lib/routes/client-api/metrics.test.ts b/src/lib/routes/client-api/metrics.test.ts index 6046b9f008ad..9bcb9682527b 100644 --- a/src/lib/routes/client-api/metrics.test.ts +++ b/src/lib/routes/client-api/metrics.test.ts @@ -188,6 +188,7 @@ test('schema allow yes=', () => { test('should return a 400 when required fields are missing', async () => { stores.featureToggleStore.create('default', { name: 'toggleLastSeen', + createdByUserId: 9999, }); await request .post('/api/client/metrics') @@ -209,6 +210,7 @@ test('should return a 400 when required fields are missing', async () => { test('should return a 200 if required fields are there', async () => { stores.featureToggleStore.create('default', { name: 'theOtherToggleLastSeen', + createdByUserId: 9999, }); await request .post('/api/client/metrics') diff --git a/src/lib/schema/feature-schema.ts b/src/lib/schema/feature-schema.ts index d64694afa1da..dfdd98efd154 100644 --- a/src/lib/schema/feature-schema.ts +++ b/src/lib/schema/feature-schema.ts @@ -104,6 +104,7 @@ export const featureMetadataSchema = joi .unique((a, b) => a.name === b.name) .optional() .items(variantsSchema), + createdByUserId: joi.number(), }) .options({ allowUnknown: false, stripUnknown: true, abortEarly: false }); diff --git a/src/lib/services/state-service.test.ts b/src/lib/services/state-service.test.ts index 76c3b3ce623f..4799cc00b69a 100644 --- a/src/lib/services/state-service.test.ts +++ b/src/lib/services/state-service.test.ts @@ -43,6 +43,7 @@ async function setupV3VariantsCompatibilityScenario( const stores = createStores(); await stores.featureToggleStore.create('some-project', { name: 'Feature-with-variants', + createdByUserId: 9999, }); let sortOrder = 1; envs.forEach(async (env) => { @@ -112,6 +113,7 @@ test('should not import an existing feature', async () => { name: 'new-feature', enabled: true, strategies: [{ name: 'default' }], + createdByUserId: 9999, }, ], }; @@ -137,6 +139,7 @@ test('should not keep existing feature if drop-before-import', async () => { name: 'new-feature', enabled: true, strategies: [{ name: 'default' }], + createdByUserId: 9999, }, ], }; @@ -289,6 +292,7 @@ test('should export featureToggles', async () => { await stores.featureToggleStore.create('default', { name: 'a-feature', + createdByUserId: 9999, }); const data = await stateService.export({ includeFeatureToggles: true }); @@ -303,6 +307,7 @@ test('archived feature toggles should not be included', async () => { await stores.featureToggleStore.create('default', { name: 'a-feature', archived: true, + createdByUserId: 9999, }); const data = await stateService.export({ includeFeatureToggles: true }); @@ -315,6 +320,7 @@ test('featureStrategy connected to an archived feature toggle should not be incl await stores.featureToggleStore.create('default', { name: featureName, archived: true, + createdByUserId: 9999, }); await stores.featureStrategiesStore.createStrategyFeatureEnv({ @@ -334,6 +340,7 @@ test('featureStrategy connected to a feature should be included', async () => { const featureName = 'fstrat-feature'; await stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await stores.featureStrategiesStore.createStrategyFeatureEnv({ @@ -662,6 +669,7 @@ test('exporting to new format works', async () => { }); await stores.featureToggleStore.create('fancy', { name: 'Some-feature', + createdByUserId: 9999, }); await stores.strategyStore.createStrategy({ name: 'format', @@ -724,6 +732,7 @@ test('featureStrategies can keep existing', async () => { }); await stores.featureToggleStore.create('fancy', { name: 'Some-feature', + createdByUserId: 9999, }); await stores.strategyStore.createStrategy({ name: 'format', @@ -779,6 +788,7 @@ test('featureStrategies should not keep existing if dropBeforeImport', async () }); await stores.featureToggleStore.create('fancy', { name: 'Some-feature', + createdByUserId: 9999, }); await stores.strategyStore.createStrategy({ name: 'format', diff --git a/src/lib/services/state-service.ts b/src/lib/services/state-service.ts index fed2cfcc4781..b548e26bfdb7 100644 --- a/src/lib/services/state-service.ts +++ b/src/lib/services/state-service.ts @@ -396,7 +396,10 @@ export default class StateService { .filter(filterExisting(keepExisting, oldToggles)) .filter(filterEqual(oldToggles)) .map(async (feature) => { - await this.toggleStore.create(feature.project, feature); + await this.toggleStore.create(feature.project, { + createdByUserId: userId, + ...feature, + }); await this.featureEnvironmentStore.connectFeatureToEnvironmentsForProject( feature.name, feature.project, diff --git a/src/lib/services/version-service.test.ts b/src/lib/services/version-service.test.ts index ede3dae6069b..28244282fc01 100644 --- a/src/lib/services/version-service.test.ts +++ b/src/lib/services/version-service.test.ts @@ -181,7 +181,10 @@ test('counts toggles', async () => { await stores.settingStore.insert('unleash.enterprise.auth.oidc', { enabled: true, }); - await stores.featureToggleStore.create('project', { name: uuidv4() }); + await stores.featureToggleStore.create('project', { + name: uuidv4(), + createdByUserId: 9999, + }); await stores.strategyStore.createStrategy({ name: uuidv4(), editable: true, @@ -237,7 +240,10 @@ test('counts custom strategies', async () => { await stores.settingStore.insert('unleash.enterprise.auth.oidc', { enabled: true, }); - await stores.featureToggleStore.create('project', { name: toggleName }); + await stores.featureToggleStore.create('project', { + name: toggleName, + createdByUserId: 9999, + }); await stores.strategyStore.createStrategy({ name: strategyName, editable: true, diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 444a031885e9..5a99e70612ce 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -59,6 +59,7 @@ export interface FeatureToggleDTO { createdAt?: Date; impressionData?: boolean; variants?: IVariant[]; + createdByUserId?: number; } export interface FeatureToggle extends FeatureToggleDTO { diff --git a/src/test/e2e/api/admin/instance-admin.e2e.test.ts b/src/test/e2e/api/admin/instance-admin.e2e.test.ts index 1efa36469ac8..dea3000a5f38 100644 --- a/src/test/e2e/api/admin/instance-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/instance-admin.e2e.test.ts @@ -20,7 +20,10 @@ afterAll(async () => { }); test('should return instance statistics', async () => { - stores.featureToggleStore.create('default', { name: 'TestStats1' }); + stores.featureToggleStore.create('default', { + name: 'TestStats1', + createdByUserId: 9999, + }); return app.request .get('/api/admin/instance-admin/statistics') @@ -62,8 +65,14 @@ test('should return signed instance statistics', async () => { }); test('should return instance statistics as CVS', async () => { - stores.featureToggleStore.create('default', { name: 'TestStats2' }); - stores.featureToggleStore.create('default', { name: 'TestStats3' }); + stores.featureToggleStore.create('default', { + name: 'TestStats2', + createdByUserId: 9999, + }); + stores.featureToggleStore.create('default', { + name: 'TestStats3', + createdByUserId: 9999, + }); const res = await app.request .get('/api/admin/instance-admin/statistics/csv') diff --git a/src/test/e2e/api/admin/project/variants.e2e.test.ts b/src/test/e2e/api/admin/project/variants.e2e.test.ts index 2f70ae6a9188..47c524da4eb4 100644 --- a/src/test/e2e/api/admin/project/variants.e2e.test.ts +++ b/src/test/e2e/api/admin/project/variants.e2e.test.ts @@ -37,7 +37,10 @@ afterAll(async () => { test('Can get variants for a feature', async () => { const featureName = 'feature-variants'; const variantName = 'fancy-variant'; - await db.stores.featureToggleStore.create('default', { name: featureName }); + await db.stores.featureToggleStore.create('default', { + name: featureName, + createdByUserId: 9999, + }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( featureName, 'default', @@ -110,6 +113,7 @@ test('Can patch variants for a feature and get a response of new variant', async await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( featureName, @@ -151,6 +155,7 @@ test('Can patch variants for a feature patches all environments independently', await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( featureName, @@ -237,6 +242,7 @@ test('Can push variants to multiple environments', async () => { }); await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( featureName, @@ -339,6 +345,7 @@ test('Can add variant for a feature', async () => { await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( @@ -394,6 +401,7 @@ test('Can remove variant for a feature', async () => { await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( @@ -438,6 +446,7 @@ test('PUT overwrites current variant on feature', async () => { ]; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( featureName, @@ -492,6 +501,7 @@ test('PUTing an invalid variant throws 400 exception', async () => { const featureName = 'variants-validation-feature'; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); const invalidJson = [ @@ -518,6 +528,7 @@ test('Invalid variant in PATCH also throws 400 exception', async () => { const featureName = 'patch-validation-feature'; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( featureName, @@ -553,6 +564,7 @@ test('PATCHING with all variable weightTypes forces weights to sum to no less th const featureName = 'variants-validation-with-all-variable-weights'; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( @@ -647,6 +659,7 @@ test('PATCHING with no variable variants fails with 400', async () => { const featureName = 'variants-validation-with-no-variable-weights'; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( featureName, @@ -681,6 +694,7 @@ test('Patching with a fixed variant and variable variants splits remaining weigh const featureName = 'variants-fixed-and-variable'; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( @@ -778,6 +792,7 @@ test('Multiple fixed variants gets added together to decide how much weight vari const featureName = 'variants-multiple-fixed-and-variable'; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( @@ -829,6 +844,7 @@ test('If sum of fixed variant weight exceed 1000 fails with 400', async () => { const featureName = 'variants-fixed-weight-over-1000'; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( @@ -876,6 +892,7 @@ test('If sum of fixed variant weight equals 1000 variable variants gets weight 0 const featureName = 'variants-fixed-weight-equals-1000-no-variable-weight'; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( @@ -944,6 +961,7 @@ test('PATCH endpoint validates uniqueness of variant names', async () => { ]; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( @@ -989,6 +1007,7 @@ test('PUT endpoint validates uniqueness of variant names', async () => { const featureName = 'variants-put-uniqueness-names'; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( @@ -1025,6 +1044,7 @@ test('Variants should be sorted by their name when PUT', async () => { const featureName = 'variants-sort-by-name'; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( @@ -1074,6 +1094,7 @@ test('Variants should be sorted by name when PATCHed as well', async () => { const featureName = 'variants-patch-sort-by-name'; await db.stores.featureToggleStore.create('default', { name: featureName, + createdByUserId: 9999, }); await db.stores.featureEnvironmentStore.addEnvironmentToFeature( diff --git a/src/test/e2e/services/playground-service.test.ts b/src/test/e2e/services/playground-service.test.ts index b90fd9a3bbde..12a6325fcffc 100644 --- a/src/test/e2e/services/playground-service.test.ts +++ b/src/test/e2e/services/playground-service.test.ts @@ -118,6 +118,7 @@ export const seedDatabaseForPlaygroundTest = async ( ...feature, createdAt: undefined, variants: null, + createdByUserId: 9999, }, ); diff --git a/src/test/e2e/services/project-health-service.e2e.test.ts b/src/test/e2e/services/project-health-service.e2e.test.ts index f56f17932877..36e1118351cc 100644 --- a/src/test/e2e/services/project-health-service.e2e.test.ts +++ b/src/test/e2e/services/project-health-service.e2e.test.ts @@ -52,11 +52,13 @@ test('Project with no stale toggles should have 100% health rating', async () => name: 'health-rating-not-stale', description: 'new', stale: false, + createdByUserId: 9999, }); await stores.featureToggleStore.create('health-rating', { name: 'health-rating-not-stale-2', description: 'new too', stale: false, + createdByUserId: 9999, }); const rating = await projectHealthService.calculateHealthRating(savedProject); @@ -74,21 +76,25 @@ test('Project with two stale toggles and two non stale should have 50% health ra name: 'health-rating-2-not-stale', description: 'new', stale: false, + createdByUserId: 9999, }); await stores.featureToggleStore.create('health-rating-2', { name: 'health-rating-2-not-stale-2', description: 'new too', stale: false, + createdByUserId: 9999, }); await stores.featureToggleStore.create('health-rating-2', { name: 'health-rating-2-stale-1', description: 'stale', stale: true, + createdByUserId: 9999, }); await stores.featureToggleStore.create('health-rating-2', { name: 'health-rating-2-stale-2', description: 'stale too', stale: true, + createdByUserId: 9999, }); const rating = await projectHealthService.calculateHealthRating(savedProject); @@ -106,6 +112,7 @@ test('Project with one non-stale, one potentially stale and one stale should hav name: 'health-rating-3-not-stale', description: 'new', stale: false, + createdByUserId: 9999, }); await stores.featureToggleStore.create('health-rating-3', { name: 'health-rating-3-potentially-stale', @@ -113,11 +120,13 @@ test('Project with one non-stale, one potentially stale and one stale should hav type: 'release', stale: false, createdAt: new Date(Date.UTC(2020, 1, 1)), + createdByUserId: 9999, }); await stores.featureToggleStore.create('health-rating-3', { name: 'health-rating-3-stale', description: 'stale', stale: true, + createdByUserId: 9999, }); const rating = await projectHealthService.calculateHealthRating(savedProject); diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index bff66f5df070..9058923d4163 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -177,6 +177,7 @@ test('should not be able to delete project with toggles', async () => { await projectService.createProject(project, user); await stores.featureToggleStore.create(project.id, { name: 'test-project-delete', + createdByUserId: 9999, }); try { @@ -1461,9 +1462,11 @@ test('should only count active feature toggles for project', async () => { await stores.featureToggleStore.create(project.id, { name: 'only-active-t1', + createdByUserId: 9999, }); await stores.featureToggleStore.create(project.id, { name: 'only-active-t2', + createdByUserId: 9999, }); await featureToggleService.archiveToggle('only-active-t2', user); @@ -1486,6 +1489,7 @@ test('should list projects with all features archived', async () => { await stores.featureToggleStore.create(project.id, { name: 'archived-toggle', + createdByUserId: 9999, }); await featureToggleService.archiveToggle('archived-toggle', user); @@ -2088,6 +2092,7 @@ test('deleting a project with archived toggles should result in any remaining ar await stores.featureToggleStore.create(project.id, { name: toggleName, + createdByUserId: 9999, }); await stores.featureToggleStore.archive(toggleName); diff --git a/src/test/e2e/stores/feature-environment-store.e2e.test.ts b/src/test/e2e/stores/feature-environment-store.e2e.test.ts index c3a91e7d8846..e87f69b0f377 100644 --- a/src/test/e2e/stores/feature-environment-store.e2e.test.ts +++ b/src/test/e2e/stores/feature-environment-store.e2e.test.ts @@ -31,7 +31,10 @@ test('Setting enabled to same as existing value returns 0', async () => { enabled: true, type: 'test', }); - await featureStore.create('default', { name: featureName }); + await featureStore.create('default', { + name: featureName, + createdByUserId: 9999, + }); await featureEnvironmentStore.connectProject(envName, 'default'); await featureEnvironmentStore.connectFeatures(envName, 'default'); const enabledStatus = await featureEnvironmentStore.isEnvironmentEnabled( @@ -54,7 +57,10 @@ test('Setting enabled to not existing value returns 1', async () => { enabled: true, type: 'test', }); - await featureStore.create('default', { name: featureName }); + await featureStore.create('default', { + name: featureName, + createdByUserId: 9999, + }); await featureEnvironmentStore.connectProject(envName, 'default'); await featureEnvironmentStore.connectFeatures(envName, 'default'); const enabledStatus = await featureEnvironmentStore.isEnvironmentEnabled( diff --git a/src/test/e2e/stores/feature-tag-store.e2e.test.ts b/src/test/e2e/stores/feature-tag-store.e2e.test.ts index 018e2b5a70c3..9692ad7f5ed0 100644 --- a/src/test/e2e/stores/feature-tag-store.e2e.test.ts +++ b/src/test/e2e/stores/feature-tag-store.e2e.test.ts @@ -19,7 +19,10 @@ beforeAll(async () => { featureTagStore = stores.featureTagStore; featureToggleStore = stores.featureToggleStore; await stores.tagStore.createTag(tag); - await featureToggleStore.create('default', { name: featureName }); + await featureToggleStore.create('default', { + name: featureName, + createdByUserId: 9999, + }); }); afterAll(async () => { @@ -80,6 +83,7 @@ test('get all feature tags', async () => { await featureTagStore.tagFeature(featureName, tag, TESTUSERID); await featureToggleStore.create('default', { name: 'some-other-toggle', + createdByUserId: 9999, }); await featureTagStore.tagFeature('some-other-toggle', tag, TESTUSERID); const all = await featureTagStore.getAll(); @@ -89,6 +93,7 @@ test('get all feature tags', async () => { test('should import feature tags', async () => { await featureToggleStore.create('default', { name: 'some-other-toggle-import', + createdByUserId: 9999, }); await featureTagStore.tagFeatures([ { @@ -121,7 +126,7 @@ test('should throw not found error if feature does not exist', async () => { test('Returns empty tag list for existing feature with no tags', async () => { const name = 'feature.with.no.tags'; - await featureToggleStore.create('default', { name }); + await featureToggleStore.create('default', { name, createdByUserId: 9999 }); const tags = await featureTagStore.getAllTagsForFeature(name); expect(tags).toHaveLength(0); });