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:
parent
0869e39603
commit
c3a00c07e1
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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 [];
|
||||
}
|
||||
}
|
@ -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,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@ -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) },
|
||||
]);
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
export type Collaborator = {
|
||||
id: number;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
};
|
||||
|
||||
export interface IFeatureCollaboratorsReadModel {
|
||||
getFeatureCollaborators(feature: string): Promise<Array<Collaborator>>;
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -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(),
|
||||
};
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user