mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-06 01:15:28 +02: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 { FeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model';
|
||||||
import { LargestResourcesReadModel } from '../features/metrics/sizes/largest-resources-read-model';
|
import { LargestResourcesReadModel } from '../features/metrics/sizes/largest-resources-read-model';
|
||||||
import { IntegrationEventsStore } from '../features/integration-events/integration-events-store';
|
import { IntegrationEventsStore } from '../features/integration-events/integration-events-store';
|
||||||
|
import { FeatureCollaboratorsReadModel } from '../features/feature-toggle/feature-collaborators-read-model';
|
||||||
|
|
||||||
export const createStores = (
|
export const createStores = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -175,6 +176,7 @@ export const createStores = (
|
|||||||
),
|
),
|
||||||
largestResourcesReadModel: new LargestResourcesReadModel(db),
|
largestResourcesReadModel: new LargestResourcesReadModel(db),
|
||||||
integrationEventsStore: new IntegrationEventsStore(db, { eventBus }),
|
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 { IFeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model-type';
|
||||||
import { ILargestResourcesReadModel } from '../features/metrics/sizes/largest-resources-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 type { IntegrationEventsStore } from '../features/integration-events/integration-events-store';
|
||||||
|
import { IFeatureCollaboratorsReadModel } from '../features/feature-toggle/types/feature-collaborators-read-model-type';
|
||||||
|
|
||||||
export interface IUnleashStores {
|
export interface IUnleashStores {
|
||||||
accessStore: IAccessStore;
|
accessStore: IAccessStore;
|
||||||
@ -98,6 +99,7 @@ export interface IUnleashStores {
|
|||||||
featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
||||||
largestResourcesReadModel: ILargestResourcesReadModel;
|
largestResourcesReadModel: ILargestResourcesReadModel;
|
||||||
integrationEventsStore: IntegrationEventsStore;
|
integrationEventsStore: IntegrationEventsStore;
|
||||||
|
featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -147,5 +149,6 @@ export {
|
|||||||
IFeatureStrategiesReadModel,
|
IFeatureStrategiesReadModel,
|
||||||
IFeatureLifecycleReadModel,
|
IFeatureLifecycleReadModel,
|
||||||
ILargestResourcesReadModel,
|
ILargestResourcesReadModel,
|
||||||
|
IFeatureCollaboratorsReadModel,
|
||||||
type IntegrationEventsStore,
|
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 { FakeFeatureStrategiesReadModel } from '../../lib/features/feature-toggle/fake-feature-strategies-read-model';
|
||||||
import { FakeFeatureLifecycleReadModel } from '../../lib/features/feature-lifecycle/fake-feature-lifecycle-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 { 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 = {
|
const db = {
|
||||||
select: () => ({
|
select: () => ({
|
||||||
@ -109,6 +110,7 @@ const createStores: () => IUnleashStores = () => {
|
|||||||
featureLifecycleReadModel: new FakeFeatureLifecycleReadModel(),
|
featureLifecycleReadModel: new FakeFeatureLifecycleReadModel(),
|
||||||
largestResourcesReadModel: new FakeLargestResourcesReadModel(),
|
largestResourcesReadModel: new FakeLargestResourcesReadModel(),
|
||||||
integrationEventsStore: {} as IntegrationEventsStore,
|
integrationEventsStore: {} as IntegrationEventsStore,
|
||||||
|
featureCollaboratorsReadModel: new FakeFeatureCollaboratorsReadModel(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user