mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
Project owners read model - db read (#6916)
Implementation of the logic for fetching project owners. Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
parent
e6355f4676
commit
477da7d514
@ -197,6 +197,10 @@ export default class ProjectController extends Controller {
|
|||||||
user.id,
|
user.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// if (this.flagResolver.isEnabled('projectsListNewCards')) {
|
||||||
|
// TODO: get project owners and add to response
|
||||||
|
// }
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
200,
|
200,
|
||||||
res,
|
res,
|
||||||
|
175
src/lib/features/project/project-owners-read-model.test.ts
Normal file
175
src/lib/features/project/project-owners-read-model.test.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||||
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
|
import { type IUser, RoleName, type IGroup } from '../../types';
|
||||||
|
import { randomId } from '../../util';
|
||||||
|
import { ProjectOwnersReadModel } from './project-owners-read-model';
|
||||||
|
|
||||||
|
describe('unit tests', () => {
|
||||||
|
test('maps owners to projects', () => {});
|
||||||
|
test('returns "system" when a project has no owners', async () => {
|
||||||
|
// this is a mapping test; not an integration test
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let db: ITestDb;
|
||||||
|
let readModel: ProjectOwnersReadModel;
|
||||||
|
|
||||||
|
let ownerRoleId: number;
|
||||||
|
let owner: IUser;
|
||||||
|
let member: IUser;
|
||||||
|
let group: IGroup;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await dbInit('project_owners_read_model_serial', getLogger);
|
||||||
|
readModel = new ProjectOwnersReadModel(db.rawDatabase, db.stores.roleStore);
|
||||||
|
ownerRoleId = (await db.stores.roleStore.getRoleByName(RoleName.OWNER)).id;
|
||||||
|
|
||||||
|
const ownerData = {
|
||||||
|
name: 'Owner User',
|
||||||
|
username: 'owner',
|
||||||
|
email: 'owner@email.com',
|
||||||
|
imageUrl: 'image-url-1',
|
||||||
|
};
|
||||||
|
const memberData = {
|
||||||
|
name: 'Member Name',
|
||||||
|
username: 'member',
|
||||||
|
email: 'member@email.com',
|
||||||
|
imageUrl: 'image-url-2',
|
||||||
|
};
|
||||||
|
|
||||||
|
// create users
|
||||||
|
owner = await db.stores.userStore.insert(ownerData);
|
||||||
|
member = await db.stores.userStore.insert(memberData);
|
||||||
|
|
||||||
|
// create groups
|
||||||
|
group = await db.stores.groupStore.create({ name: 'Group Name' });
|
||||||
|
await db.stores.groupStore.addUserToGroups(owner.id, [group.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (db) {
|
||||||
|
await db.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('integration tests', () => {
|
||||||
|
test('name takes precedence over username', async () => {
|
||||||
|
const projectId = randomId();
|
||||||
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
||||||
|
|
||||||
|
await db.stores.accessStore.addUserToRole(
|
||||||
|
owner.id,
|
||||||
|
ownerRoleId,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const owners = await readModel.getAllProjectOwners();
|
||||||
|
expect(owners).toMatchObject({
|
||||||
|
[projectId]: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ name: 'Owner User' }),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gets project user owners', async () => {
|
||||||
|
const projectId = randomId();
|
||||||
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
||||||
|
|
||||||
|
await db.stores.accessStore.addUserToRole(
|
||||||
|
owner.id,
|
||||||
|
ownerRoleId,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const owners = await readModel.getAllProjectOwners();
|
||||||
|
|
||||||
|
expect(owners).toMatchObject({
|
||||||
|
[projectId]: [
|
||||||
|
{
|
||||||
|
ownerType: 'user',
|
||||||
|
name: 'Owner User',
|
||||||
|
email: 'owner@email.com',
|
||||||
|
imageUrl: 'image-url-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not get regular project members', async () => {
|
||||||
|
const projectId = randomId();
|
||||||
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
||||||
|
|
||||||
|
const memberRole = await db.stores.roleStore.getRoleByName(
|
||||||
|
RoleName.MEMBER,
|
||||||
|
);
|
||||||
|
await db.stores.accessStore.addUserToRole(
|
||||||
|
owner.id,
|
||||||
|
ownerRoleId,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.stores.accessStore.addUserToRole(
|
||||||
|
member.id,
|
||||||
|
memberRole.id,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const owners = await readModel.getAllProjectOwners();
|
||||||
|
|
||||||
|
expect(owners).toMatchObject({
|
||||||
|
[projectId]: [{ name: 'Owner User' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gets project group owners', async () => {
|
||||||
|
const projectId = randomId();
|
||||||
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
||||||
|
|
||||||
|
await db.stores.accessStore.addGroupToRole(
|
||||||
|
group.id,
|
||||||
|
ownerRoleId,
|
||||||
|
'',
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const owners = await readModel.getAllProjectOwners();
|
||||||
|
|
||||||
|
expect(owners).toMatchObject({
|
||||||
|
[projectId]: [
|
||||||
|
{
|
||||||
|
ownerType: 'group',
|
||||||
|
name: 'Group Name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('users are listed before groups', async () => {});
|
||||||
|
|
||||||
|
test('owners (users and groups) are sorted by when they were added; oldest first', async () => {});
|
||||||
|
|
||||||
|
test('returns the system owner for the default project', async () => {});
|
||||||
|
|
||||||
|
test('returns an empty list if there are no projects', async () => {
|
||||||
|
const owners = await readModel.getAllProjectOwners();
|
||||||
|
|
||||||
|
expect(owners).toStrictEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enriches fully', async () => {
|
||||||
|
const owners = await readModel.enrichWithOwners([]);
|
||||||
|
|
||||||
|
expect(owners).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
});
|
151
src/lib/features/project/project-owners-read-model.ts
Normal file
151
src/lib/features/project/project-owners-read-model.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import type { Db } from '../../db/db';
|
||||||
|
import { RoleName, type IProjectWithCount, type IRoleStore } from '../../types';
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
ROLE_USER: 'role_user',
|
||||||
|
GROUP_ROLE: 'group_role',
|
||||||
|
ROLES: 'roles',
|
||||||
|
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 {
|
||||||
|
private db: Db;
|
||||||
|
roleStore: IRoleStore;
|
||||||
|
|
||||||
|
constructor(db: Db, roleStore: IRoleStore) {
|
||||||
|
this.db = db;
|
||||||
|
this.roleStore = roleStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
addOwnerData(
|
||||||
|
projects: IProjectWithCount[],
|
||||||
|
owners: ProjectOwnersDictionary,
|
||||||
|
): IProjectWithCountAndOwners[] {
|
||||||
|
// const projectsWithOwners = projects.map((p) => ({
|
||||||
|
// ...p,
|
||||||
|
// owners: projectOwners[p.id] || [],
|
||||||
|
// }));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAllProjectUsersByRole(
|
||||||
|
roleId: number,
|
||||||
|
): Promise<Record<string, UserProjectOwner[]>> {
|
||||||
|
const usersResult = await this.db
|
||||||
|
.select(
|
||||||
|
'user.username',
|
||||||
|
'user.name',
|
||||||
|
'user.email',
|
||||||
|
'user.image_url',
|
||||||
|
'ru.created_at',
|
||||||
|
'ru.project',
|
||||||
|
)
|
||||||
|
.from(`${T.ROLE_USER} as ru`)
|
||||||
|
.join(`${T.ROLES} as r`, 'ru.role_id', 'r.id')
|
||||||
|
.where('r.id', roleId)
|
||||||
|
.join(`${T.USERS} as user`, 'ru.user_id', 'user.id');
|
||||||
|
const usersDict: Record<string, UserProjectOwner[]> = {};
|
||||||
|
|
||||||
|
usersResult.forEach((user) => {
|
||||||
|
const project = user.project as string;
|
||||||
|
|
||||||
|
const data: UserProjectOwner = {
|
||||||
|
ownerType: 'user',
|
||||||
|
name: user?.name || user?.username,
|
||||||
|
email: user?.email,
|
||||||
|
imageUrl: user?.image_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (project in usersDict) {
|
||||||
|
usersDict[project] = [...usersDict[project], data];
|
||||||
|
} else {
|
||||||
|
usersDict[project] = [data];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return usersDict;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAllProjectGroupsByRole(
|
||||||
|
roleId: number,
|
||||||
|
): Promise<Record<string, GroupProjectOwner[]>> {
|
||||||
|
const groupsResult = await this.db
|
||||||
|
.select('groups.name', 'gr.created_at', 'gr.project')
|
||||||
|
.from(`${T.GROUP_ROLE} as gr`)
|
||||||
|
.join(`${T.ROLES} as r`, 'gr.role_id', 'r.id')
|
||||||
|
.where('r.id', roleId)
|
||||||
|
.join('groups', 'gr.group_id', 'groups.id');
|
||||||
|
|
||||||
|
const groupsDict: Record<string, GroupProjectOwner[]> = {};
|
||||||
|
|
||||||
|
groupsResult.forEach((group) => {
|
||||||
|
const project = group.project as string;
|
||||||
|
|
||||||
|
const data: GroupProjectOwner = {
|
||||||
|
ownerType: 'group',
|
||||||
|
name: group?.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (project in groupsDict) {
|
||||||
|
groupsDict[project] = [...groupsDict[project], data];
|
||||||
|
} else {
|
||||||
|
groupsDict[project] = [data];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupsDict;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllProjectOwners(): Promise<ProjectOwnersDictionary> {
|
||||||
|
const ownerRole = await this.roleStore.getRoleByName(RoleName.OWNER);
|
||||||
|
const usersDict = await this.getAllProjectUsersByRole(ownerRole.id);
|
||||||
|
const groupsDict = await this.getAllProjectGroupsByRole(ownerRole.id);
|
||||||
|
|
||||||
|
const projects = [
|
||||||
|
...new Set([...Object.keys(usersDict), ...Object.keys(groupsDict)]),
|
||||||
|
];
|
||||||
|
|
||||||
|
const dict = Object.fromEntries(
|
||||||
|
projects.map((project) => {
|
||||||
|
return [
|
||||||
|
project,
|
||||||
|
[
|
||||||
|
...(usersDict[project] || []),
|
||||||
|
...(groupsDict[project] || []),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
async enrichWithOwners(
|
||||||
|
projects: IProjectWithCount[],
|
||||||
|
): Promise<IProjectWithCountAndOwners[]> {
|
||||||
|
const owners = await this.getAllProjectOwners();
|
||||||
|
|
||||||
|
return this.addOwnerData(projects, owners);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user