Skip to content

Commit

Permalink
feat: project flag creators api (#7302)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Jun 6, 2024
1 parent 63f3212 commit 3c3e888
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/lib/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
};
};
Expand Down
6 changes: 6 additions & 0 deletions src/lib/features/project/createProjectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -119,6 +122,7 @@ export const createProjectService = (
accountStore,
projectStatsStore,
projectOwnersReadModel,
projectFlagCreatorsReadModel,
},
config,
accessService,
Expand All @@ -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();
Expand Down Expand Up @@ -175,6 +180,7 @@ export const createFakeProjectService = (
{
projectStore,
projectOwnersReadModel,
projectFlagCreatorsReadModel,
eventStore,
featureToggleStore,
environmentStore,
Expand Down
11 changes: 11 additions & 0 deletions src/lib/features/project/fake-project-flag-creators-read-model.ts
Original file line number Diff line number Diff line change
@@ -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 [];
}
}
42 changes: 42 additions & 0 deletions src/lib/features/project/project-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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>,
Expand Down
31 changes: 31 additions & 0 deletions src/lib/features/project/project-flag-creators-read-model.ts
Original file line number Diff line number Diff line change
@@ -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),
}));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IProjectFlagCreatorsReadModel {
getFlagCreators(
project: string,
): Promise<Array<{ id: number; name: string }>>;
}
61 changes: 61 additions & 0 deletions src/lib/features/project/project-flag-creators.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
})
.expect(200);
await app.createFeature('flag-name-1');
await app.request
.post(`/auth/demo/login`)
.send({
email: '[email protected]',
})
.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: '[email protected]' },
{ id: 2, name: '[email protected]' },
]);
});
10 changes: 10 additions & 0 deletions src/lib/features/project/project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -114,6 +115,8 @@ export default class ProjectService {

private projectOwnersReadModel: IProjectOwnersReadModel;

private projectFlagCreatorsReadModel: IProjectFlagCreatorsReadModel;

private accessService: AccessService;

private eventStore: IEventStore;
Expand Down Expand Up @@ -150,6 +153,7 @@ export default class ProjectService {
{
projectStore,
projectOwnersReadModel,
projectFlagCreatorsReadModel,
eventStore,
featureToggleStore,
environmentStore,
Expand All @@ -161,6 +165,7 @@ export default class ProjectService {
IUnleashStores,
| 'projectStore'
| 'projectOwnersReadModel'
| 'projectFlagCreatorsReadModel'
| 'eventStore'
| 'featureToggleStore'
| 'environmentStore'
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/lib/openapi/spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
33 changes: 33 additions & 0 deletions src/lib/openapi/spec/project-flag-creators-schema.ts
Original file line number Diff line number Diff line change
@@ -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
>;
3 changes: 3 additions & 0 deletions src/lib/types/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -87,6 +88,7 @@ export interface IUnleashStores {
trafficDataUsageStore: ITrafficDataUsageStore;
segmentReadModel: ISegmentReadModel;
projectOwnersReadModel: IProjectOwnersReadModel;
projectFlagCreatorsReadModel: IProjectFlagCreatorsReadModel;
featureLifecycleStore: IFeatureLifecycleStore;
}

Expand Down Expand Up @@ -133,4 +135,5 @@ export {
ISegmentReadModel,
IProjectOwnersReadModel,
IFeatureLifecycleStore,
IProjectFlagCreatorsReadModel,
};
2 changes: 2 additions & 0 deletions src/test/fixtures/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => ({
Expand Down Expand Up @@ -98,6 +99,7 @@ const createStores: () => IUnleashStores = () => {
trafficDataUsageStore: new FakeTrafficDataUsageStore(),
segmentReadModel: new FakeSegmentReadModel(),
projectOwnersReadModel: new FakeProjectOwnersReadModel(),
projectFlagCreatorsReadModel: new FakeProjectFlagCreatorsReadModel(),
featureLifecycleStore: new FakeFeatureLifecycleStore(),
};
};
Expand Down

0 comments on commit 3c3e888

Please sign in to comment.