1
0
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:
David Leek 2024-01-25 13:09:30 +01:00 committed by GitHub
parent 8ab4aa3d0e
commit c7f13aec0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 259 additions and 2 deletions

View File

@ -79,6 +79,7 @@ exports[`should create default config 1`] = `
"caseInsensitiveInOperators": false,
"celebrateUnleash": false,
"changeRequestConflictHandling": false,
"createdByUserIdDataMigration": true,
"customRootRolesKillSwitch": false,
"demo": false,
"detectSegmentUsageInChangeRequests": false,

View File

@ -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.');
}
}

View File

@ -2402,6 +2402,10 @@ class FeatureToggleService {
);
}
}
async setFeatureCreatedByUserIdFromEvents(): Promise<void> {
await this.featureToggleStore.setCreatedByUserId(100);
}
}
export default FeatureToggleService;

View File

@ -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;

View File

@ -103,4 +103,6 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
getFeatureTypeCounts(
params: IFeatureProjectUserParams,
): Promise<IFeatureTypeCount[]>;
setCreatedByUserId(batchSize: number): Promise<void>;
}

View File

@ -150,4 +150,12 @@ export const scheduleServices = async (
minutesToMilliseconds(3),
'updateAccountLastSeen',
);
schedulerService.schedule(
featureToggleService.setFeatureCreatedByUserIdFromEvents.bind(
featureToggleService,
),
minutesToMilliseconds(15),
'setFeatureCreatedByUserIdFromEvents',
);
};

View File

@ -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 = {

View File

@ -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);
});