Skip to content

Commit

Permalink
feat: project owners in project service (#6935)
Browse files Browse the repository at this point in the history
Schema and integrating into service and controller for project owners

---------

Co-authored-by: Thomas Heartman <thomas@getunleash.io>
  • Loading branch information
Tymek and thomasheartman committed Apr 26, 2024
1 parent 7d01dbb commit 66ec9a2
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 48 deletions.
2 changes: 2 additions & 0 deletions src/lib/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import FeatureSearchStore from '../features/feature-search/feature-search-store'
import { InactiveUsersStore } from '../users/inactive/inactive-users-store';
import { TrafficDataUsageStore } from '../features/traffic-data-usage/traffic-data-usage-store';
import { SegmentReadModel } from '../features/segment/segment-read-model';
import { ProjectOwnersReadModel } from '../features/project/project-owners-read-model';

export const createStores = (
config: IUnleashConfig,
Expand Down Expand Up @@ -148,6 +149,7 @@ export const createStores = (
inactiveUsersStore: new InactiveUsersStore(db, eventBus, getLogger),
trafficDataUsageStore: new TrafficDataUsageStore(db, getLogger),
segmentReadModel: new SegmentReadModel(db),
projectOwnersReadModel: new ProjectOwnersReadModel(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 @@ -41,6 +41,8 @@ import {
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';
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';

export const createProjectService = (
db: Db,
Expand All @@ -54,6 +56,7 @@ export const createProjectService = (
getLogger,
flagResolver,
);
const projectOwnersReadModel = new ProjectOwnersReadModel(db);
const groupStore = new GroupStore(db);
const featureToggleStore = new FeatureToggleStore(
db,
Expand Down Expand Up @@ -115,6 +118,7 @@ export const createProjectService = (
featureTypeStore,
accountStore,
projectStatsStore,
projectOwnersReadModel,
},
config,
accessService,
Expand All @@ -131,6 +135,7 @@ export const createFakeProjectService = (
): ProjectService => {
const { getLogger } = config;
const eventStore = new FakeEventStore();
const projectOwnersReadModel = new FakeProjectOwnersReadModel();
const projectStore = new FakeProjectStore();
const groupStore = new FakeGroupStore();
const featureToggleStore = new FakeFeatureToggleStore();
Expand Down Expand Up @@ -169,6 +174,7 @@ export const createFakeProjectService = (
return new ProjectService(
{
projectStore,
projectOwnersReadModel,
eventStore,
featureToggleStore,
environmentStore,
Expand Down
16 changes: 16 additions & 0 deletions src/lib/features/project/fake-project-owners-read-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { IProjectWithCount } from '../../types';
import type {
IProjectOwnersReadModel,
IProjectWithCountAndOwners,
} from './project-owners-read-model.type';

export class FakeProjectOwnersReadModel implements IProjectOwnersReadModel {
async addOwners(
projects: IProjectWithCount[],
): Promise<IProjectWithCountAndOwners[]> {
return projects.map((project) => ({
...project,
owners: [{ ownerType: 'system' }],
}));
}
}
16 changes: 13 additions & 3 deletions src/lib/features/project/project-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,19 @@ export default class ProjectController extends Controller {
user.id,
);

// if (this.flagResolver.isEnabled('projectsListNewCards')) {
// TODO: get project owners and add to response
// }
if (this.flagResolver.isEnabled('projectsListNewCards')) {
const projectsWithOwners =
await this.projectService.addOwnersToProjects(projects);

this.openApiService.respondWithValidation(
200,
res,
projectsSchema.$id,
{ version: 1, projects: serializeDates(projectsWithOwners) },
);

return;
}

this.openApiService.respondWithValidation(
200,
Expand Down
40 changes: 25 additions & 15 deletions src/lib/features/project/project-owners-read-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ const mockProjectWithCounts = (name: string) => ({

describe('unit tests', () => {
test('maps owners to projects', () => {
const projects = [{ name: 'project1' }, { name: 'project2' }] as any;
const projects = [
{ id: 'project1', name: 'Project one' },
{ id: 'project2', name: 'Project two' },
] as any;

const owners = {
project1: [{ ownerType: 'user' as const, name: 'Owner Name' }],
Expand All @@ -32,13 +35,21 @@ describe('unit tests', () => {
);

expect(projectsWithOwners).toMatchObject([
{ name: 'project1', owners: [{ name: 'Owner Name' }] },
{ name: 'project2', owners: [{ name: 'Owner Name' }] },
{
id: 'project1',
name: 'Project one',
owners: [{ name: 'Owner Name' }],
},
{
id: 'project2',
name: 'Project two',
owners: [{ name: 'Owner Name' }],
},
]);
});

test('returns "system" when a project has no owners', async () => {
const projects = [{ name: 'project1' }, { name: 'project2' }] as any;
const projects = [{ id: 'project1' }, { id: 'project2' }] as any;

const owners = {};

Expand All @@ -48,8 +59,14 @@ describe('unit tests', () => {
);

expect(projectsWithOwners).toMatchObject([
{ name: 'project1', owners: [{ ownerType: 'system' }] },
{ name: 'project2', owners: [{ ownerType: 'system' }] },
{
id: 'project1',
owners: [{ ownerType: 'system' }],
},
{
id: 'project2',
owners: [{ ownerType: 'system' }],
},
]);
});
});
Expand All @@ -66,7 +83,7 @@ let group2: IGroup;

beforeAll(async () => {
db = await dbInit('project_owners_read_model_serial', getLogger);
readModel = new ProjectOwnersReadModel(db.rawDatabase, db.stores.roleStore);
readModel = new ProjectOwnersReadModel(db.rawDatabase);
ownerRoleId = (await db.stores.roleStore.getRoleByName(RoleName.OWNER)).id;

const ownerData = {
Expand Down Expand Up @@ -107,14 +124,7 @@ afterAll(async () => {
});

afterEach(async () => {
if (db) {
const projects = await db.stores.projectStore.getAll();
for (const project of projects) {
// Clean only project roles, not all roles
await db.stores.roleStore.removeRolesForProject(project.id);
}
await db.stores.projectStore.deleteAll();
}
db.stores.roleStore;
});

describe('integration tests', () => {
Expand Down
42 changes: 14 additions & 28 deletions src/lib/features/project/project-owners-read-model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { Db } from '../../db/db';
import { RoleName, type IProjectWithCount, type IRoleStore } from '../../types';
import { RoleName, type IProjectWithCount } from '../../types';
import type {
GroupProjectOwner,
IProjectOwnersReadModel,
IProjectWithCountAndOwners,
ProjectOwnersDictionary,
UserProjectOwner,
} from './project-owners-read-model.type';

const T = {
ROLE_USER: 'role_user',
Expand All @@ -8,34 +15,11 @@ const T = {
USERS: 'users',
};

type SystemOwner = { ownerType: 'system' };
type UserProjectOwner = {
ownerType: 'user';
name: string;
email?: string;
imageUrl?: string;
};
type GroupProjectOwner = {
ownerType: 'group';
name: string;
};
type ProjectOwners =
| [SystemOwner]
| Array<UserProjectOwner | GroupProjectOwner>;

export type ProjectOwnersDictionary = Record<string, ProjectOwners>;

type IProjectWithCountAndOwners = IProjectWithCount & {
owners: ProjectOwners;
};

export class ProjectOwnersReadModel {
export class ProjectOwnersReadModel implements IProjectOwnersReadModel {
private db: Db;
roleStore: IRoleStore;

constructor(db: Db, roleStore: IRoleStore) {
constructor(db: Db) {
this.db = db;
this.roleStore = roleStore;
}

static addOwnerData(
Expand All @@ -44,7 +28,7 @@ export class ProjectOwnersReadModel {
): IProjectWithCountAndOwners[] {
return projects.map((project) => ({
...project,
owners: owners[project.name] || [{ ownerType: 'system' }],
owners: owners[project.id] || [{ ownerType: 'system' }],
}));
}

Expand Down Expand Up @@ -119,7 +103,9 @@ export class ProjectOwnersReadModel {
}

async getAllProjectOwners(): Promise<ProjectOwnersDictionary> {
const ownerRole = await this.roleStore.getRoleByName(RoleName.OWNER);
const ownerRole = await this.db(T.ROLES)
.where({ name: RoleName.OWNER })
.first();
const usersDict = await this.getAllProjectUsersByRole(ownerRole.id);
const groupsDict = await this.getAllProjectGroupsByRole(ownerRole.id);

Expand Down
28 changes: 28 additions & 0 deletions src/lib/features/project/project-owners-read-model.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { IProjectWithCount } from '../../types';

export type SystemOwner = { ownerType: 'system' };
export type UserProjectOwner = {
ownerType: 'user';
name: string;
email?: string;
imageUrl?: string;
};
export type GroupProjectOwner = {
ownerType: 'group';
name: string;
};
type ProjectOwners =
| [SystemOwner]
| Array<UserProjectOwner | GroupProjectOwner>;

export type ProjectOwnersDictionary = Record<string, ProjectOwners>;

export type IProjectWithCountAndOwners = IProjectWithCount & {
owners: ProjectOwners;
};

export interface IProjectOwnersReadModel {
addOwners(
projects: IProjectWithCount[],
): Promise<IProjectWithCountAndOwners[]>;
}
14 changes: 12 additions & 2 deletions src/lib/features/project/project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
RoleName,
SYSTEM_USER_ID,
type ProjectCreated,
type IProjectOwnersReadModel,
} from '../../types';
import type {
IProjectAccessModel,
Expand Down Expand Up @@ -77,8 +78,6 @@ import type {
IProjectQuery,
} from './project-store-type';

const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';

type Days = number;
type Count = number;

Expand Down Expand Up @@ -112,6 +111,8 @@ function includes(
export default class ProjectService {
private projectStore: IProjectStore;

private projectOwnersReadModel: IProjectOwnersReadModel;

private accessService: AccessService;

private eventStore: IEventStore;
Expand Down Expand Up @@ -147,6 +148,7 @@ export default class ProjectService {
constructor(
{
projectStore,
projectOwnersReadModel,
eventStore,
featureToggleStore,
environmentStore,
Expand All @@ -157,6 +159,7 @@ export default class ProjectService {
}: Pick<
IUnleashStores,
| 'projectStore'
| 'projectOwnersReadModel'
| 'eventStore'
| 'featureToggleStore'
| 'environmentStore'
Expand All @@ -174,6 +177,7 @@ export default class ProjectService {
privateProjectChecker: IPrivateProjectChecker,
) {
this.projectStore = projectStore;
this.projectOwnersReadModel = projectOwnersReadModel;
this.environmentStore = environmentStore;
this.featureEnvironmentStore = featureEnvironmentStore;
this.accessService = accessService;
Expand Down Expand Up @@ -218,6 +222,12 @@ export default class ProjectService {
return projects;
}

async addOwnersToProjects(
projects: IProjectWithCount[],
): Promise<IProjectWithCount[]> {
return this.projectOwnersReadModel.addOwners(projects);
}

async getProject(id: string): Promise<IProject> {
return this.projectStore.get(id);
}
Expand Down
Loading

0 comments on commit 66ec9a2

Please sign in to comment.