mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: event creators (#7809)
Adds an endpoint to return all event creators. An interesting point is that it does not return the user object, but just created_by as a string. This is because we do not store user IDs for events, as they are not strictly bound to a user object, but rather a historical user with the name X.
This commit is contained in:
parent
bde81b940c
commit
2f92dac14e
@ -200,4 +200,8 @@ export default class EventService {
|
||||
|
||||
return queryParams;
|
||||
};
|
||||
|
||||
async getEventCreators() {
|
||||
return this.eventStore.getEventCreators();
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import { sharedEventEmitter } from '../../util/anyEventEmitter';
|
||||
import type { Db } from '../../db/db';
|
||||
import type { Knex } from 'knex';
|
||||
import type EventEmitter from 'events';
|
||||
import { ADMIN_TOKEN_USER, SYSTEM_USER_ID } from '../../types';
|
||||
import { ADMIN_TOKEN_USER, SYSTEM_USER, SYSTEM_USER_ID } from '../../types';
|
||||
import type { DeprecatedSearchEventsSchema } from '../../openapi';
|
||||
import type { IQueryParam } from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||
import { applyGenericQueryParams } from '../feature-search/search-utils';
|
||||
@ -380,6 +380,32 @@ class EventStore implements IEventStore {
|
||||
return query;
|
||||
}
|
||||
|
||||
async getEventCreators(): Promise<Array<{ id: number; name: string }>> {
|
||||
const query = this.db('events')
|
||||
.distinct('events.created_by_user_id')
|
||||
.leftJoin('users', 'users.id', '=', 'events.created_by_user_id')
|
||||
.select([
|
||||
'events.created_by_user_id as id',
|
||||
this.db.raw(`
|
||||
CASE
|
||||
WHEN events.created_by_user_id = -1337 THEN '${SYSTEM_USER.name}'
|
||||
WHEN events.created_by_user_id = -42 THEN '${ADMIN_TOKEN_USER.name}'
|
||||
ELSE COALESCE(users.name, events.created_by)
|
||||
END as name
|
||||
`),
|
||||
'users.username',
|
||||
'users.email',
|
||||
]);
|
||||
|
||||
const result = await query;
|
||||
return result
|
||||
.filter((row) => row.name || row.username || row.email)
|
||||
.map((row) => ({
|
||||
id: Number(row.id),
|
||||
name: String(row.name || row.username || row.email),
|
||||
}));
|
||||
}
|
||||
|
||||
async deprecatedSearchEvents(
|
||||
search: DeprecatedSearchEventsSchema = {},
|
||||
): Promise<IEvent[]> {
|
||||
|
31
src/lib/openapi/spec/event-creators-schema.ts
Normal file
31
src/lib/openapi/spec/event-creators-schema.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const eventCreatorsSchema = {
|
||||
$id: '#/components/schemas/eventCreatorsSchema',
|
||||
type: 'array',
|
||||
description: 'A list of event creators',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['id', 'name'],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
example: 50,
|
||||
description: 'The user id.',
|
||||
},
|
||||
name: {
|
||||
description:
|
||||
"Name of the user. If the user has no set name, the API falls back to using the user's username (if they have one) or email (if neither name or username is set).",
|
||||
type: 'string',
|
||||
example: 'User',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
schemas: {},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type EventCreatorsSchema = FromSchema<typeof eventCreatorsSchema>;
|
@ -69,6 +69,7 @@ export * from './environment-project-schema';
|
||||
export * from './environment-schema';
|
||||
export * from './environments-project-schema';
|
||||
export * from './environments-schema';
|
||||
export * from './event-creators-schema';
|
||||
export * from './event-schema';
|
||||
export * from './event-search-response-schema';
|
||||
export * from './events-schema';
|
||||
|
@ -21,6 +21,11 @@ import { getStandardResponses } from '../../../lib/openapi/util/standard-respons
|
||||
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||
import type { DeprecatedSearchEventsSchema } from '../../openapi/spec/deprecated-search-events-schema';
|
||||
import type { IFlagResolver } from '../../types/experimental';
|
||||
import type { IAuthRequest } from '../unleash-types';
|
||||
import {
|
||||
eventCreatorsSchema,
|
||||
type ProjectFlagCreatorsSchema,
|
||||
} from '../../openapi';
|
||||
|
||||
const ANON_KEYS = ['email', 'username', 'createdBy'];
|
||||
const version = 1 as const;
|
||||
@ -45,7 +50,7 @@ export default class EventController extends Controller {
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '',
|
||||
path: '/events',
|
||||
handler: this.getEvents,
|
||||
permission: ADMIN,
|
||||
middleware: [
|
||||
@ -76,7 +81,7 @@ export default class EventController extends Controller {
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:featureName',
|
||||
path: '/events/:featureName',
|
||||
handler: this.getEventsForToggle,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
@ -97,7 +102,7 @@ export default class EventController extends Controller {
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/search',
|
||||
path: '/events/search',
|
||||
handler: this.deprecatedSearchEvents,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
@ -115,6 +120,26 @@ export default class EventController extends Controller {
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/event-creators',
|
||||
handler: this.getEventCreators,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
this.openApiService.validPath({
|
||||
tags: ['Events'],
|
||||
operationId: 'getEventCreators',
|
||||
summary: 'Get a list of all users that have created events',
|
||||
description:
|
||||
'Returns a list of all users that have created events in the system.',
|
||||
responses: {
|
||||
200: createResponseSchema('eventCreatorsSchema'),
|
||||
...getStandardResponses(401, 403, 404),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
maybeAnonymiseEvents(events: IEvent[]): IEvent[] {
|
||||
@ -197,4 +222,18 @@ export default class EventController extends Controller {
|
||||
response,
|
||||
);
|
||||
}
|
||||
|
||||
async getEventCreators(
|
||||
req: IAuthRequest,
|
||||
res: Response<ProjectFlagCreatorsSchema>,
|
||||
): Promise<void> {
|
||||
const flagCreators = await this.eventService.getEventCreators();
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
eventCreatorsSchema.$id,
|
||||
serializeDates(flagCreators),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ export class AdminApi extends Controller {
|
||||
'/strategies',
|
||||
new StrategyController(config, services).router,
|
||||
);
|
||||
this.app.use('/events', new EventController(config, services).router);
|
||||
this.app.use('', new EventController(config, services).router);
|
||||
this.app.use(
|
||||
'/playground',
|
||||
new PlaygroundController(config, services).router,
|
||||
|
@ -20,5 +20,10 @@ export class SearchApi extends Controller {
|
||||
'/events',
|
||||
new EventSearchController(config, services).router,
|
||||
);
|
||||
|
||||
this.app.use(
|
||||
'/events',
|
||||
new EventSearchController(config, services).router,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -43,4 +43,5 @@ export interface IEventStore
|
||||
query(operations: IQueryOperations[]): Promise<IEvent[]>;
|
||||
queryCount(operations: IQueryOperations[]): Promise<number>;
|
||||
setCreatedByUserId(batchSize: number): Promise<number | undefined>;
|
||||
getEventCreators(): Promise<Array<{ id: number; name: string }>>;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import { FEATURE_CREATED, type IBaseEvent } from '../../../../lib/types/events';
|
||||
import { randomId } from '../../../../lib/util/random-id';
|
||||
import { EventService } from '../../../../lib/services';
|
||||
import EventEmitter from 'events';
|
||||
import { SYSTEM_USER } from '../../../../lib/types';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
@ -150,3 +151,61 @@ test('can search for events', async () => {
|
||||
expect(res.body.events[0].data.id).toEqual(events[1].data.id);
|
||||
});
|
||||
});
|
||||
|
||||
test('event creators - if system user, return system name, else should return name from database if user exists, else from events table', async () => {
|
||||
const user = await db.stores.userStore.insert({ name: 'database-user' });
|
||||
const events: IBaseEvent[] = [
|
||||
{
|
||||
type: FEATURE_CREATED,
|
||||
project: randomId(),
|
||||
data: { id: randomId() },
|
||||
tags: [],
|
||||
createdBy: 'should-not-use-this-name',
|
||||
createdByUserId: SYSTEM_USER.id,
|
||||
ip: '127.0.0.1',
|
||||
},
|
||||
{
|
||||
type: FEATURE_CREATED,
|
||||
project: randomId(),
|
||||
data: { id: randomId() },
|
||||
tags: [],
|
||||
createdBy: 'test-user1',
|
||||
createdByUserId: user.id,
|
||||
ip: '127.0.0.1',
|
||||
},
|
||||
{
|
||||
type: FEATURE_CREATED,
|
||||
project: randomId(),
|
||||
data: { id: randomId() },
|
||||
preData: { id: randomId() },
|
||||
tags: [{ type: 'simple', value: randomId() }],
|
||||
createdBy: 'test-user2',
|
||||
createdByUserId: 2,
|
||||
ip: '127.0.0.1',
|
||||
},
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
events.map((event) => {
|
||||
return eventService.storeEvent(event);
|
||||
}),
|
||||
);
|
||||
|
||||
const { body } = await app.request
|
||||
.get('/api/admin/event-creators')
|
||||
.expect(200);
|
||||
expect(body).toMatchObject([
|
||||
{
|
||||
id: SYSTEM_USER.id,
|
||||
name: SYSTEM_USER.name,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'database-user',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'test-user2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
4
src/test/fixtures/fake-event-store.ts
vendored
4
src/test/fixtures/fake-event-store.ts
vendored
@ -15,6 +15,10 @@ class FakeEventStore implements IEventStore {
|
||||
this.events = [];
|
||||
}
|
||||
|
||||
getEventCreators(): Promise<{ id: number; name: string }[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getMaxRevisionId(): Promise<number> {
|
||||
return Promise.resolve(1);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user