mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-29 01:15:48 +02:00
chore: implement created_by_user_id in features (#5994)
## About the changes Adds a scheduled task that every 5 seconds updates 500 entries in the features table setting `created_by_user_id`. It does this by looking at the related event, checks created_by and joins users table for match on username or email, and joins api_tokens table on username matches. Then picks either a users id if set, or uses -42 (admin token user)
This commit is contained in:
parent
8ab4aa3d0e
commit
c7f13aec0b
@ -79,6 +79,7 @@ exports[`should create default config 1`] = `
|
|||||||
"caseInsensitiveInOperators": false,
|
"caseInsensitiveInOperators": false,
|
||||||
"celebrateUnleash": false,
|
"celebrateUnleash": false,
|
||||||
"changeRequestConflictHandling": false,
|
"changeRequestConflictHandling": false,
|
||||||
|
"createdByUserIdDataMigration": true,
|
||||||
"customRootRolesKillSwitch": false,
|
"customRootRolesKillSwitch": false,
|
||||||
"demo": false,
|
"demo": false,
|
||||||
"detectSegmentUsageInChangeRequests": false,
|
"detectSegmentUsageInChangeRequests": false,
|
||||||
|
@ -328,4 +328,8 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
|||||||
): Promise<IFeatureTypeCount[]> {
|
): Promise<IFeatureTypeCount[]> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCreatedByUserId(batchSize: number): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2402,6 +2402,10 @@ class FeatureToggleService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setFeatureCreatedByUserIdFromEvents(): Promise<void> {
|
||||||
|
await this.featureToggleStore.setCreatedByUserId(100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FeatureToggleService;
|
export default FeatureToggleService;
|
||||||
|
@ -18,7 +18,11 @@ import { DEFAULT_ENV } from '../../../lib/util';
|
|||||||
|
|
||||||
import { FeatureToggleListBuilder } from './query-builders/feature-toggle-list-builder';
|
import { FeatureToggleListBuilder } from './query-builders/feature-toggle-list-builder';
|
||||||
import { FeatureConfigurationClient } from './types/feature-toggle-strategies-store-type';
|
import { FeatureConfigurationClient } from './types/feature-toggle-strategies-store-type';
|
||||||
import { IFeatureTypeCount, IFlagResolver } from '../../../lib/types';
|
import {
|
||||||
|
ADMIN_TOKEN_USER,
|
||||||
|
IFeatureTypeCount,
|
||||||
|
IFlagResolver,
|
||||||
|
} from '../../../lib/types';
|
||||||
import { FeatureToggleRowConverter } from './converters/feature-toggle-row-converter';
|
import { FeatureToggleRowConverter } from './converters/feature-toggle-row-converter';
|
||||||
import { IFeatureProjectUserParams } from './feature-toggle-controller';
|
import { IFeatureProjectUserParams } from './feature-toggle-controller';
|
||||||
|
|
||||||
@ -718,6 +722,40 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
|||||||
|
|
||||||
return result?.potentially_stale ?? false;
|
return result?.potentially_stale ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setCreatedByUserId(batchSize: number): Promise<void> {
|
||||||
|
const EVENTS_TABLE = 'events';
|
||||||
|
const USERS_TABLE = 'users';
|
||||||
|
const API_TOKEN_TABLE = 'api_tokens';
|
||||||
|
|
||||||
|
if (!this.flagResolver.isEnabled('createdByUserIdDataMigration')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toUpdate = await this.db(`${TABLE} as f`)
|
||||||
|
.joinRaw(`JOIN ${EVENTS_TABLE} AS ev ON ev.feature_name = f.name`)
|
||||||
|
.joinRaw(
|
||||||
|
`LEFT OUTER JOIN ${USERS_TABLE} AS u on ev.created_by = u.username OR ev.created_by = u.email`,
|
||||||
|
)
|
||||||
|
.joinRaw(
|
||||||
|
`LEFT OUTER JOIN ${API_TOKEN_TABLE} AS t on ev.created_by = t.username`,
|
||||||
|
)
|
||||||
|
.whereRaw(
|
||||||
|
`f.created_by_user_id IS null AND ev.type = 'feature-created'`,
|
||||||
|
)
|
||||||
|
.orderBy('f.created_at', 'asc')
|
||||||
|
.limit(batchSize)
|
||||||
|
.select(['f.*', 'ev.created_by', 'u.id', 't.username']);
|
||||||
|
|
||||||
|
toUpdate
|
||||||
|
.filter((row) => row.id || row.username)
|
||||||
|
.forEach(async (row) => {
|
||||||
|
const id = row.id || ADMIN_TOKEN_USER.id;
|
||||||
|
|
||||||
|
await this.db(TABLE)
|
||||||
|
.update({ created_by_user_id: id })
|
||||||
|
.where({ name: row.name });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = FeatureToggleStore;
|
module.exports = FeatureToggleStore;
|
||||||
|
@ -103,4 +103,6 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
|||||||
getFeatureTypeCounts(
|
getFeatureTypeCounts(
|
||||||
params: IFeatureProjectUserParams,
|
params: IFeatureProjectUserParams,
|
||||||
): Promise<IFeatureTypeCount[]>;
|
): Promise<IFeatureTypeCount[]>;
|
||||||
|
|
||||||
|
setCreatedByUserId(batchSize: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -150,4 +150,12 @@ export const scheduleServices = async (
|
|||||||
minutesToMilliseconds(3),
|
minutesToMilliseconds(3),
|
||||||
'updateAccountLastSeen',
|
'updateAccountLastSeen',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
schedulerService.schedule(
|
||||||
|
featureToggleService.setFeatureCreatedByUserIdFromEvents.bind(
|
||||||
|
featureToggleService,
|
||||||
|
),
|
||||||
|
minutesToMilliseconds(15),
|
||||||
|
'setFeatureCreatedByUserIdFromEvents',
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -46,7 +46,8 @@ export type IFlagKey =
|
|||||||
| 'adminTokenKillSwitch'
|
| 'adminTokenKillSwitch'
|
||||||
| 'changeRequestConflictHandling'
|
| 'changeRequestConflictHandling'
|
||||||
| 'executiveDashboard'
|
| 'executiveDashboard'
|
||||||
| 'feedbackComments';
|
| 'feedbackComments'
|
||||||
|
| 'createdByUserIdDataMigration';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -222,6 +223,10 @@ const flags: IFlags = {
|
|||||||
'',
|
'',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdByUserIdDataMigration: parseEnvVarBoolean(
|
||||||
|
process.env.CREATED_BY_USERID_DATA_MIGRATION,
|
||||||
|
true,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -0,0 +1,195 @@
|
|||||||
|
import { createFeatureToggleService } from '../../lib/features';
|
||||||
|
import { EventService, FeatureToggleService } from '../../lib/services';
|
||||||
|
import {
|
||||||
|
ADMIN_TOKEN_USER,
|
||||||
|
IUnleashConfig,
|
||||||
|
IUnleashStores,
|
||||||
|
} from '../../lib/types';
|
||||||
|
import { createTestConfig } from '../config/test-config';
|
||||||
|
import dbInit, { ITestDb } from './helpers/database-init';
|
||||||
|
|
||||||
|
let stores: IUnleashStores;
|
||||||
|
let db: ITestDb;
|
||||||
|
let service: FeatureToggleService;
|
||||||
|
let eventService: EventService;
|
||||||
|
let unleashConfig: IUnleashConfig;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const config = createTestConfig();
|
||||||
|
db = await dbInit(
|
||||||
|
'features_created_by_user_id_migration',
|
||||||
|
config.getLogger,
|
||||||
|
);
|
||||||
|
unleashConfig = config;
|
||||||
|
stores = db.stores;
|
||||||
|
|
||||||
|
service = createFeatureToggleService(db.rawDatabase, config);
|
||||||
|
|
||||||
|
eventService = new EventService(stores, config);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await db.rawDatabase('features').del();
|
||||||
|
await db.rawDatabase('events').del();
|
||||||
|
await db.rawDatabase('users').del();
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.rawDatabase('features').del();
|
||||||
|
await db.rawDatabase('events').del();
|
||||||
|
await db.rawDatabase('users').del();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should set created_by_user_id on features', async () => {
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
await db.rawDatabase('features').insert({
|
||||||
|
name: `feature${i}`,
|
||||||
|
type: 'release',
|
||||||
|
project: 'default',
|
||||||
|
description: '--created_by_test--',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.rawDatabase('users').insert({
|
||||||
|
username: 'test1',
|
||||||
|
});
|
||||||
|
await db.rawDatabase('users').insert({
|
||||||
|
username: 'test2',
|
||||||
|
});
|
||||||
|
await db.rawDatabase('users').insert({
|
||||||
|
username: 'test3',
|
||||||
|
});
|
||||||
|
await db.rawDatabase('users').insert({
|
||||||
|
username: 'test4',
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
await db.rawDatabase('events').insert({
|
||||||
|
type: 'feature-created',
|
||||||
|
created_by: 'test1',
|
||||||
|
feature_name: `feature${i}`,
|
||||||
|
data: `{"name":"feature${i}","description":null,"type":"release","project":"default","stale":false,"createdAt":"2024-01-08T10:36:32.866Z","lastSeenAt":null,"impressionData":false,"archivedAt":null,"archived":false}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 25; i < 50; i++) {
|
||||||
|
await db.rawDatabase('events').insert({
|
||||||
|
type: 'feature-created',
|
||||||
|
created_by: 'test2',
|
||||||
|
feature_name: `feature${i}`,
|
||||||
|
data: `{"name":"feature${i}","description":null,"type":"release","project":"default","stale":false,"createdAt":"2024-01-08T10:36:32.866Z","lastSeenAt":null,"impressionData":false,"archivedAt":null,"archived":false}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 50; i < 75; i++) {
|
||||||
|
await db.rawDatabase('events').insert({
|
||||||
|
type: 'feature-created',
|
||||||
|
created_by: 'test3',
|
||||||
|
feature_name: `feature${i}`,
|
||||||
|
data: `{"name":"feature${i}","description":null,"type":"release","project":"default","stale":false,"createdAt":"2024-01-08T10:36:32.866Z","lastSeenAt":null,"impressionData":false,"archivedAt":null,"archived":false}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 75; i < 100; i++) {
|
||||||
|
await db.rawDatabase('events').insert({
|
||||||
|
type: 'feature-created',
|
||||||
|
created_by: 'test4',
|
||||||
|
feature_name: `feature${i}`,
|
||||||
|
data: `{"name":"feature${i}","description":null,"type":"release","project":"default","stale":false,"createdAt":"2024-01-08T10:36:32.866Z","lastSeenAt":null,"impressionData":false,"archivedAt":null,"archived":false}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await stores.featureToggleStore.setCreatedByUserId(200);
|
||||||
|
|
||||||
|
const features = await db.rawDatabase('features').select('*');
|
||||||
|
const notSet = features.filter(
|
||||||
|
(f) => !f.created_by_user_id && f.description === '--created_by_test--',
|
||||||
|
);
|
||||||
|
const test1 = features.filter((f) => f.created_by_user_id === 1);
|
||||||
|
const test2 = features.filter((f) => f.created_by_user_id === 2);
|
||||||
|
const test3 = features.filter((f) => f.created_by_user_id === 3);
|
||||||
|
const test4 = features.filter((f) => f.created_by_user_id === 4);
|
||||||
|
expect(notSet).toHaveLength(0);
|
||||||
|
expect(test1).toHaveLength(25);
|
||||||
|
expect(test2).toHaveLength(25);
|
||||||
|
expect(test3).toHaveLength(25);
|
||||||
|
expect(test4).toHaveLength(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin tokens get populated to admin token user', async () => {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await db.rawDatabase('features').insert({
|
||||||
|
name: `feature${i}`,
|
||||||
|
type: 'release',
|
||||||
|
project: 'default',
|
||||||
|
description: '--created_by_test--',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.rawDatabase('users').insert({
|
||||||
|
username: 'input1',
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.rawDatabase('api_tokens').insert({
|
||||||
|
secret: 'token1',
|
||||||
|
username: 'adm-token',
|
||||||
|
type: 'admin',
|
||||||
|
environment: 'default',
|
||||||
|
token_name: 'admin-token',
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.rawDatabase('events').insert({
|
||||||
|
type: 'feature-created',
|
||||||
|
created_by: 'input1',
|
||||||
|
feature_name: 'feature0',
|
||||||
|
data: `{"name":"feature0","description":null,"type":"release","project":"default","stale":false,"createdAt":"2024-01-08T10:36:32.866Z","lastSeenAt":null,"impressionData":false,"archivedAt":null,"archived":false}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.rawDatabase('events').insert({
|
||||||
|
type: 'feature-created',
|
||||||
|
created_by: 'input1',
|
||||||
|
feature_name: 'feature1',
|
||||||
|
data: `{"name":"feature1","description":null,"type":"release","project":"default","stale":false,"createdAt":"2024-01-08T10:36:32.866Z","lastSeenAt":null,"impressionData":false,"archivedAt":null,"archived":false}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.rawDatabase('events').insert({
|
||||||
|
type: 'feature-created',
|
||||||
|
created_by: 'adm-token',
|
||||||
|
feature_name: 'feature2',
|
||||||
|
data: `{"name":"feature2","description":null,"type":"release","project":"default","stale":false,"createdAt":"2024-01-08T10:36:32.866Z","lastSeenAt":null,"impressionData":false,"archivedAt":null,"archived":false}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.rawDatabase('events').insert({
|
||||||
|
type: 'feature-created',
|
||||||
|
created_by: 'deleted-user',
|
||||||
|
feature_name: 'feature3',
|
||||||
|
data: `{"name":"feature3","description":null,"type":"release","project":"default","stale":false,"createdAt":"2024-01-08T10:36:32.866Z","lastSeenAt":null,"impressionData":false,"archivedAt":null,"archived":false}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.rawDatabase('events').insert({
|
||||||
|
type: 'feature-created',
|
||||||
|
created_by: 'adm-token',
|
||||||
|
feature_name: 'feature4',
|
||||||
|
data: `{"name":"feature4","description":null,"type":"release","project":"default","stale":false,"createdAt":"2024-01-08T10:36:32.866Z","lastSeenAt":null,"impressionData":false,"archivedAt":null,"archived":false}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await stores.featureToggleStore.setCreatedByUserId(200);
|
||||||
|
|
||||||
|
const user = await db
|
||||||
|
.rawDatabase('users')
|
||||||
|
.where({ username: 'input1' })
|
||||||
|
.first('id');
|
||||||
|
|
||||||
|
const features = await db.rawDatabase('features').select('*');
|
||||||
|
const notSet = features.filter(
|
||||||
|
(f) => !f.created_by_user_id && f.description === '--created_by_test--',
|
||||||
|
);
|
||||||
|
const test1 = features.filter((f) => f.created_by_user_id === user.id);
|
||||||
|
const test2 = features.filter(
|
||||||
|
(f) => f.created_by_user_id === ADMIN_TOKEN_USER.id,
|
||||||
|
);
|
||||||
|
expect(notSet).toHaveLength(1);
|
||||||
|
expect(test1).toHaveLength(2);
|
||||||
|
expect(test2).toHaveLength(2);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user