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,
|
||||
"celebrateUnleash": false,
|
||||
"changeRequestConflictHandling": false,
|
||||
"createdByUserIdDataMigration": true,
|
||||
"customRootRolesKillSwitch": false,
|
||||
"demo": false,
|
||||
"detectSegmentUsageInChangeRequests": false,
|
||||
|
@ -328,4 +328,8 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
||||
): Promise<IFeatureTypeCount[]> {
|
||||
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;
|
||||
|
@ -18,7 +18,11 @@ import { DEFAULT_ENV } from '../../../lib/util';
|
||||
|
||||
import { FeatureToggleListBuilder } from './query-builders/feature-toggle-list-builder';
|
||||
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 { IFeatureProjectUserParams } from './feature-toggle-controller';
|
||||
|
||||
@ -718,6 +722,40 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
|
||||
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;
|
||||
|
@ -103,4 +103,6 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
||||
getFeatureTypeCounts(
|
||||
params: IFeatureProjectUserParams,
|
||||
): Promise<IFeatureTypeCount[]>;
|
||||
|
||||
setCreatedByUserId(batchSize: number): Promise<void>;
|
||||
}
|
||||
|
@ -150,4 +150,12 @@ export const scheduleServices = async (
|
||||
minutesToMilliseconds(3),
|
||||
'updateAccountLastSeen',
|
||||
);
|
||||
|
||||
schedulerService.schedule(
|
||||
featureToggleService.setFeatureCreatedByUserIdFromEvents.bind(
|
||||
featureToggleService,
|
||||
),
|
||||
minutesToMilliseconds(15),
|
||||
'setFeatureCreatedByUserIdFromEvents',
|
||||
);
|
||||
};
|
||||
|
@ -46,7 +46,8 @@ export type IFlagKey =
|
||||
| 'adminTokenKillSwitch'
|
||||
| 'changeRequestConflictHandling'
|
||||
| 'executiveDashboard'
|
||||
| 'feedbackComments';
|
||||
| 'feedbackComments'
|
||||
| 'createdByUserIdDataMigration';
|
||||
|
||||
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 = {
|
||||
|
@ -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