1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: feature collaborators read model (#7625)

This commit is contained in:
Mateusz Kwasniewski 2024-07-19 12:10:21 +02:00 committed by GitHub
parent 0869e39603
commit c3a00c07e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 150 additions and 0 deletions

View File

@ -50,6 +50,7 @@ import { FeatureStrategiesReadModel } from '../features/feature-toggle/feature-s
import { FeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model';
import { LargestResourcesReadModel } from '../features/metrics/sizes/largest-resources-read-model';
import { IntegrationEventsStore } from '../features/integration-events/integration-events-store';
import { FeatureCollaboratorsReadModel } from '../features/feature-toggle/feature-collaborators-read-model';
export const createStores = (
config: IUnleashConfig,
@ -175,6 +176,7 @@ export const createStores = (
),
largestResourcesReadModel: new LargestResourcesReadModel(db),
integrationEventsStore: new IntegrationEventsStore(db, { eventBus }),
featureCollaboratorsReadModel: new FeatureCollaboratorsReadModel(db),
};
};

View File

@ -0,0 +1,14 @@
import type {
Collaborator,
IFeatureCollaboratorsReadModel,
} from './types/feature-collaborators-read-model-type';
export class FakeFeatureCollaboratorsReadModel
implements IFeatureCollaboratorsReadModel
{
async getFeatureCollaborators(
feature: string,
): Promise<Array<Collaborator>> {
return [];
}
}

View File

@ -0,0 +1,49 @@
import type { Db } from '../../db/db';
import type {
Collaborator,
IFeatureCollaboratorsReadModel,
} from './types/feature-collaborators-read-model-type';
import { generateImageUrl } from '../../util';
export class FeatureCollaboratorsReadModel
implements IFeatureCollaboratorsReadModel
{
private db: Db;
constructor(db: Db) {
this.db = db;
}
async getFeatureCollaborators(
feature: string,
): Promise<Array<Collaborator>> {
const query = this.db
.with('recent_events', (queryBuilder) => {
queryBuilder
.select('created_by_user_id')
.max('created_at as max_created_at')
.from('events')
.where('feature_name', feature)
.groupBy('created_by_user_id');
})
.select('users.id', 'users.email', 'users.username', 'users.name')
.from('recent_events')
.join('users', 'recent_events.created_by_user_id', 'users.id')
.orderBy('recent_events.max_created_at', 'desc');
const rows = await query;
return rows.map((row) => {
const name = row.name || row.username || row.email || 'unknown';
return {
id: row.id,
name: name,
imageUrl: generateImageUrl({
id: row.id,
email: row.email,
username: name,
}),
};
});
}
}

View File

@ -0,0 +1,71 @@
import type {
IEventStore,
IFeatureCollaboratorsReadModel,
IUnleashStores,
IUserStore,
} from '../../../types';
import getLogger from '../../../../test/fixtures/no-logger';
import dbInit, {
type ITestDb,
} from '../../../../test/e2e/helpers/database-init';
let stores: IUnleashStores;
let db: ITestDb;
let eventStore: IEventStore;
let usersStore: IUserStore;
let featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
beforeAll(async () => {
db = await dbInit('feature_collaborators_read_model', getLogger);
stores = db.stores;
eventStore = stores.eventStore;
usersStore = stores.userStore;
featureCollaboratorsReadModel = stores.featureCollaboratorsReadModel;
});
afterAll(async () => {
await db.destroy();
});
test('Should return collaborators according to their activity order', async () => {
const user1 = await usersStore.insert({
name: 'User One',
email: 'user1@example.com',
});
const user2 = await usersStore.insert({
name: 'User Two',
email: 'user2@example.com',
});
// first event on our feature
await eventStore.store({
featureName: 'featureA',
createdByUserId: user1.id,
type: 'feature-created',
createdBy: 'irrelevant',
ip: '::1',
});
// first event on another feature
await eventStore.store({
featureName: 'featureB',
createdByUserId: user1.id,
type: 'feature-created',
createdBy: 'irrelevant',
ip: '::1',
});
// second event on our feature
await eventStore.store({
featureName: 'featureA',
createdByUserId: user2.id,
type: 'feature-updated',
createdBy: 'irrelevant',
ip: '::1',
});
const collaborators =
await featureCollaboratorsReadModel.getFeatureCollaborators('featureA');
expect(collaborators).toMatchObject([
{ id: 2, name: 'User Two', imageUrl: expect.any(String) },
{ id: 1, name: 'User One', imageUrl: expect.any(String) },
]);
});

View File

@ -0,0 +1,9 @@
export type Collaborator = {
id: number;
name: string;
imageUrl: string;
};
export interface IFeatureCollaboratorsReadModel {
getFeatureCollaborators(feature: string): Promise<Array<Collaborator>>;
}

View File

@ -47,6 +47,7 @@ import { IFeatureStrategiesReadModel } from '../features/feature-toggle/types/fe
import { IFeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model-type';
import { ILargestResourcesReadModel } from '../features/metrics/sizes/largest-resources-read-model-type';
import type { IntegrationEventsStore } from '../features/integration-events/integration-events-store';
import { IFeatureCollaboratorsReadModel } from '../features/feature-toggle/types/feature-collaborators-read-model-type';
export interface IUnleashStores {
accessStore: IAccessStore;
@ -98,6 +99,7 @@ export interface IUnleashStores {
featureLifecycleReadModel: IFeatureLifecycleReadModel;
largestResourcesReadModel: ILargestResourcesReadModel;
integrationEventsStore: IntegrationEventsStore;
featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
}
export {
@ -147,5 +149,6 @@ export {
IFeatureStrategiesReadModel,
IFeatureLifecycleReadModel,
ILargestResourcesReadModel,
IFeatureCollaboratorsReadModel,
type IntegrationEventsStore,
};

View File

@ -50,6 +50,7 @@ import { FakeProjectFlagCreatorsReadModel } from '../../lib/features/project/fak
import { FakeFeatureStrategiesReadModel } from '../../lib/features/feature-toggle/fake-feature-strategies-read-model';
import { FakeFeatureLifecycleReadModel } from '../../lib/features/feature-lifecycle/fake-feature-lifecycle-read-model';
import { FakeLargestResourcesReadModel } from '../../lib/features/metrics/sizes/fake-largest-resources-read-model';
import { FakeFeatureCollaboratorsReadModel } from '../../lib/features/feature-toggle/fake-feature-collaborators-read-model';
const db = {
select: () => ({
@ -109,6 +110,7 @@ const createStores: () => IUnleashStores = () => {
featureLifecycleReadModel: new FakeFeatureLifecycleReadModel(),
largestResourcesReadModel: new FakeLargestResourcesReadModel(),
integrationEventsStore: {} as IntegrationEventsStore,
featureCollaboratorsReadModel: new FakeFeatureCollaboratorsReadModel(),
};
};