mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
feat: project flag creators api (#7302)
This commit is contained in:
parent
63f3212624
commit
3c3e888ff0
@ -45,6 +45,7 @@ import { TrafficDataUsageStore } from '../features/traffic-data-usage/traffic-da
|
||||
import { SegmentReadModel } from '../features/segment/segment-read-model';
|
||||
import { ProjectOwnersReadModel } from '../features/project/project-owners-read-model';
|
||||
import { FeatureLifecycleStore } from '../features/feature-lifecycle/feature-lifecycle-store';
|
||||
import { ProjectFlagCreatorsReadModel } from '../features/project/project-flag-creators-read-model';
|
||||
|
||||
export const createStores = (
|
||||
config: IUnleashConfig,
|
||||
@ -156,6 +157,7 @@ export const createStores = (
|
||||
trafficDataUsageStore: new TrafficDataUsageStore(db, getLogger),
|
||||
segmentReadModel: new SegmentReadModel(db),
|
||||
projectOwnersReadModel: new ProjectOwnersReadModel(db),
|
||||
projectFlagCreatorsReadModel: new ProjectFlagCreatorsReadModel(db),
|
||||
featureLifecycleStore: new FeatureLifecycleStore(db),
|
||||
};
|
||||
};
|
||||
|
@ -43,6 +43,8 @@ import FeatureTypeStore from '../../db/feature-type-store';
|
||||
import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store';
|
||||
import { ProjectOwnersReadModel } from './project-owners-read-model';
|
||||
import { FakeProjectOwnersReadModel } from './fake-project-owners-read-model';
|
||||
import { FakeProjectFlagCreatorsReadModel } from './fake-project-flag-creators-read-model';
|
||||
import { ProjectFlagCreatorsReadModel } from './project-flag-creators-read-model';
|
||||
|
||||
export const createProjectService = (
|
||||
db: Db,
|
||||
@ -57,6 +59,7 @@ export const createProjectService = (
|
||||
flagResolver,
|
||||
);
|
||||
const projectOwnersReadModel = new ProjectOwnersReadModel(db);
|
||||
const projectFlagCreatorsReadModel = new ProjectFlagCreatorsReadModel(db);
|
||||
const groupStore = new GroupStore(db);
|
||||
const featureToggleStore = new FeatureToggleStore(
|
||||
db,
|
||||
@ -119,6 +122,7 @@ export const createProjectService = (
|
||||
accountStore,
|
||||
projectStatsStore,
|
||||
projectOwnersReadModel,
|
||||
projectFlagCreatorsReadModel,
|
||||
},
|
||||
config,
|
||||
accessService,
|
||||
@ -136,6 +140,7 @@ export const createFakeProjectService = (
|
||||
const { getLogger } = config;
|
||||
const eventStore = new FakeEventStore();
|
||||
const projectOwnersReadModel = new FakeProjectOwnersReadModel();
|
||||
const projectFlagCreatorsReadModel = new FakeProjectFlagCreatorsReadModel();
|
||||
const projectStore = new FakeProjectStore();
|
||||
const groupStore = new FakeGroupStore();
|
||||
const featureToggleStore = new FakeFeatureToggleStore();
|
||||
@ -175,6 +180,7 @@ export const createFakeProjectService = (
|
||||
{
|
||||
projectStore,
|
||||
projectOwnersReadModel,
|
||||
projectFlagCreatorsReadModel,
|
||||
eventStore,
|
||||
featureToggleStore,
|
||||
environmentStore,
|
||||
|
@ -0,0 +1,11 @@
|
||||
import type { IProjectFlagCreatorsReadModel } from './project-flag-creators-read-model.type';
|
||||
|
||||
export class FakeProjectFlagCreatorsReadModel
|
||||
implements IProjectFlagCreatorsReadModel
|
||||
{
|
||||
async getFlagCreators(
|
||||
project: string,
|
||||
): Promise<{ id: number; name: string }[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
@ -44,6 +44,10 @@ import { normalizeQueryParams } from '../feature-search/search-utils';
|
||||
import ProjectInsightsController from '../project-insights/project-insights-controller';
|
||||
import FeatureLifecycleController from '../feature-lifecycle/feature-lifecycle-controller';
|
||||
import type ClientInstanceService from '../metrics/instance/instance-service';
|
||||
import {
|
||||
projectFlagCreatorsSchema,
|
||||
type ProjectFlagCreatorsSchema,
|
||||
} from '../../openapi/spec/project-flag-creators-schema';
|
||||
|
||||
export default class ProjectController extends Controller {
|
||||
private projectService: ProjectService;
|
||||
@ -166,6 +170,26 @@ export default class ProjectController extends Controller {
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:projectId/flag-creators',
|
||||
handler: this.getProjectFlagCreators,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
this.openApiService.validPath({
|
||||
tags: ['Unstable'],
|
||||
operationId: 'getProjectFlagCreators',
|
||||
summary: 'Get a list of all flag creators for a project.',
|
||||
description:
|
||||
'This endpoint returns every user who created a flag in the project.',
|
||||
responses: {
|
||||
200: createResponseSchema('projectFlagCreatorsSchema'),
|
||||
...getStandardResponses(401, 403, 404),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:projectId/sdks/outdated',
|
||||
@ -327,6 +351,24 @@ export default class ProjectController extends Controller {
|
||||
serializeDates(applications),
|
||||
);
|
||||
}
|
||||
|
||||
async getProjectFlagCreators(
|
||||
req: IAuthRequest<IProjectParam>,
|
||||
res: Response<ProjectFlagCreatorsSchema>,
|
||||
): Promise<void> {
|
||||
const { projectId } = req.params;
|
||||
|
||||
const flagCreators =
|
||||
await this.projectService.getProjectFlagCreators(projectId);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
projectFlagCreatorsSchema.$id,
|
||||
serializeDates(flagCreators),
|
||||
);
|
||||
}
|
||||
|
||||
async getOutdatedProjectSdks(
|
||||
req: IAuthRequest<IProjectParam>,
|
||||
res: Response<OutdatedSdksSchema>,
|
||||
|
31
src/lib/features/project/project-flag-creators-read-model.ts
Normal file
31
src/lib/features/project/project-flag-creators-read-model.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { Db } from '../../db/db';
|
||||
import type { IProjectFlagCreatorsReadModel } from './project-flag-creators-read-model.type';
|
||||
|
||||
export class ProjectFlagCreatorsReadModel
|
||||
implements IProjectFlagCreatorsReadModel
|
||||
{
|
||||
private db: Db;
|
||||
|
||||
constructor(db: Db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async getFlagCreators(
|
||||
project: string,
|
||||
): Promise<Array<{ id: number; name: string }>> {
|
||||
const result = await this.db('users')
|
||||
.distinct('users.id')
|
||||
.join('features', 'users.id', '=', 'features.created_by_user_id')
|
||||
.where('features.project', project)
|
||||
.select([
|
||||
'users.id',
|
||||
'users.name',
|
||||
'users.username',
|
||||
'users.email',
|
||||
]);
|
||||
return result.map((row) => ({
|
||||
id: Number(row.id),
|
||||
name: String(row.name || row.username || row.email),
|
||||
}));
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
export interface IProjectFlagCreatorsReadModel {
|
||||
getFlagCreators(
|
||||
project: string,
|
||||
): Promise<Array<{ id: number; name: string }>>;
|
||||
}
|
61
src/lib/features/project/project-flag-creators.e2e.test.ts
Normal file
61
src/lib/features/project/project-flag-creators.e2e.test.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||
import {
|
||||
type IUnleashTest,
|
||||
setupAppWithAuth,
|
||||
} from '../../../test/e2e/helpers/test-helper';
|
||||
import getLogger from '../../../test/fixtures/no-logger';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('project_flag_creators', getLogger);
|
||||
app = await setupAppWithAuth(
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
db.rawDatabase,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.stores.featureToggleStore.deleteAll();
|
||||
await db.stores.userStore.deleteAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.destroy();
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('should return flag creators', async () => {
|
||||
await app.request
|
||||
.post(`/auth/demo/login`)
|
||||
.send({
|
||||
email: 'user1@getunleash.io',
|
||||
})
|
||||
.expect(200);
|
||||
await app.createFeature('flag-name-1');
|
||||
await app.request
|
||||
.post(`/auth/demo/login`)
|
||||
.send({
|
||||
email: 'user2@getunleash.io',
|
||||
})
|
||||
.expect(200);
|
||||
await app.createFeature('flag-name-2');
|
||||
|
||||
const { body } = await app.request
|
||||
.get('/api/admin/projects/default/flag-creators')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toEqual([
|
||||
{ id: 1, name: 'user1@getunleash.io' },
|
||||
{ id: 2, name: 'user2@getunleash.io' },
|
||||
]);
|
||||
});
|
@ -78,6 +78,7 @@ import type {
|
||||
IProjectEnterpriseSettingsUpdate,
|
||||
IProjectQuery,
|
||||
} from './project-store-type';
|
||||
import type { IProjectFlagCreatorsReadModel } from './project-flag-creators-read-model.type';
|
||||
|
||||
type Days = number;
|
||||
type Count = number;
|
||||
@ -114,6 +115,8 @@ export default class ProjectService {
|
||||
|
||||
private projectOwnersReadModel: IProjectOwnersReadModel;
|
||||
|
||||
private projectFlagCreatorsReadModel: IProjectFlagCreatorsReadModel;
|
||||
|
||||
private accessService: AccessService;
|
||||
|
||||
private eventStore: IEventStore;
|
||||
@ -150,6 +153,7 @@ export default class ProjectService {
|
||||
{
|
||||
projectStore,
|
||||
projectOwnersReadModel,
|
||||
projectFlagCreatorsReadModel,
|
||||
eventStore,
|
||||
featureToggleStore,
|
||||
environmentStore,
|
||||
@ -161,6 +165,7 @@ export default class ProjectService {
|
||||
IUnleashStores,
|
||||
| 'projectStore'
|
||||
| 'projectOwnersReadModel'
|
||||
| 'projectFlagCreatorsReadModel'
|
||||
| 'eventStore'
|
||||
| 'featureToggleStore'
|
||||
| 'environmentStore'
|
||||
@ -179,6 +184,7 @@ export default class ProjectService {
|
||||
) {
|
||||
this.projectStore = projectStore;
|
||||
this.projectOwnersReadModel = projectOwnersReadModel;
|
||||
this.projectFlagCreatorsReadModel = projectFlagCreatorsReadModel;
|
||||
this.environmentStore = environmentStore;
|
||||
this.featureEnvironmentStore = featureEnvironmentStore;
|
||||
this.accessService = accessService;
|
||||
@ -1081,6 +1087,10 @@ export default class ProjectService {
|
||||
return applications;
|
||||
}
|
||||
|
||||
async getProjectFlagCreators(projectId: string) {
|
||||
return this.projectFlagCreatorsReadModel.getFlagCreators(projectId);
|
||||
}
|
||||
|
||||
async changeRole(
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
|
@ -139,6 +139,7 @@ export * from './project-application-sdk-schema';
|
||||
export * from './project-applications-schema';
|
||||
export * from './project-dora-metrics-schema';
|
||||
export * from './project-environment-schema';
|
||||
export * from './project-flag-creators-schema';
|
||||
export * from './project-insights-schema';
|
||||
export * from './project-overview-schema';
|
||||
export * from './project-schema';
|
||||
|
33
src/lib/openapi/spec/project-flag-creators-schema.ts
Normal file
33
src/lib/openapi/spec/project-flag-creators-schema.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const projectFlagCreatorsSchema = {
|
||||
$id: '#/components/schemas/projectFlagCreatorsSchema',
|
||||
type: 'array',
|
||||
description: 'A list of project flag 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 ProjectFlagCreatorsSchema = FromSchema<
|
||||
typeof projectFlagCreatorsSchema
|
||||
>;
|
@ -42,6 +42,7 @@ import { ITrafficDataUsageStore } from '../features/traffic-data-usage/traffic-d
|
||||
import { ISegmentReadModel } from '../features/segment/segment-read-model-type';
|
||||
import { IProjectOwnersReadModel } from '../features/project/project-owners-read-model.type';
|
||||
import { IFeatureLifecycleStore } from '../features/feature-lifecycle/feature-lifecycle-store-type';
|
||||
import { IProjectFlagCreatorsReadModel } from '../features/project/project-flag-creators-read-model.type';
|
||||
|
||||
export interface IUnleashStores {
|
||||
accessStore: IAccessStore;
|
||||
@ -87,6 +88,7 @@ export interface IUnleashStores {
|
||||
trafficDataUsageStore: ITrafficDataUsageStore;
|
||||
segmentReadModel: ISegmentReadModel;
|
||||
projectOwnersReadModel: IProjectOwnersReadModel;
|
||||
projectFlagCreatorsReadModel: IProjectFlagCreatorsReadModel;
|
||||
featureLifecycleStore: IFeatureLifecycleStore;
|
||||
}
|
||||
|
||||
@ -133,4 +135,5 @@ export {
|
||||
ISegmentReadModel,
|
||||
IProjectOwnersReadModel,
|
||||
IFeatureLifecycleStore,
|
||||
IProjectFlagCreatorsReadModel,
|
||||
};
|
||||
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -45,6 +45,7 @@ import { FakeTrafficDataUsageStore } from '../../lib/features/traffic-data-usage
|
||||
import { FakeSegmentReadModel } from '../../lib/features/segment/fake-segment-read-model';
|
||||
import { FakeProjectOwnersReadModel } from '../../lib/features/project/fake-project-owners-read-model';
|
||||
import { FakeFeatureLifecycleStore } from '../../lib/features/feature-lifecycle/fake-feature-lifecycle-store';
|
||||
import { FakeProjectFlagCreatorsReadModel } from '../../lib/features/project/fake-project-flag-creators-read-model';
|
||||
|
||||
const db = {
|
||||
select: () => ({
|
||||
@ -98,6 +99,7 @@ const createStores: () => IUnleashStores = () => {
|
||||
trafficDataUsageStore: new FakeTrafficDataUsageStore(),
|
||||
segmentReadModel: new FakeSegmentReadModel(),
|
||||
projectOwnersReadModel: new FakeProjectOwnersReadModel(),
|
||||
projectFlagCreatorsReadModel: new FakeProjectFlagCreatorsReadModel(),
|
||||
featureLifecycleStore: new FakeFeatureLifecycleStore(),
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user