Skip to content

Commit

Permalink
project owners get all groups
Browse files Browse the repository at this point in the history
  • Loading branch information
Tymek committed Apr 24, 2024
1 parent 6b8247c commit 26a9581
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 42 deletions.
81 changes: 65 additions & 16 deletions src/lib/features/project/project-owners-read-model.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
import getLogger from '../../../test/fixtures/no-logger';
import { type IUser, RoleName } from '../../types';
import { type IUser, RoleName, type IGroup } from '../../types';
import { randomId } from '../../util';
import { ProjectOwnersReadModel } from './project-owners-read-model';

Expand All @@ -14,21 +14,24 @@ describe('unit tests', () => {
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',
name: 'Owner User',
username: 'owner',
email: '[email protected]',
imageUrl: 'image-url-1',
};
const memberData = {
name: 'member',
name: 'Member Name',
username: 'member',
email: '[email protected]',
imageUrl: 'image-url-2',
Expand All @@ -37,6 +40,10 @@ beforeAll(async () => {
// 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 () => {
Expand All @@ -57,35 +64,51 @@ afterEach(async () => {
});

describe('integration tests', () => {
test('name takes precedence over username', () => {
// import { extractUsername } from '../../../util/extract-user';
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 });

const ownerRole = await db.stores.roleStore.getRoleByName(
RoleName.OWNER,
);
await db.stores.accessStore.addUserToRole(
owner.id,
ownerRole.id,
ownerRoleId,
projectId,
);

// fetch project owners
const owners = await readModel.getAllProjectOwners();

expect(owners).toMatchObject({
[projectId]: [{ name: 'owner' }],
[projectId]: [
{
ownerType: 'user',
name: 'Owner User',
email: '[email protected]',
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 ownerRole = await db.stores.roleStore.getRoleByName(
Expand All @@ -97,7 +120,7 @@ describe('integration tests', () => {
);
await db.stores.accessStore.addUserToRole(
owner.id,
ownerRole.id,
ownerRoleId,
projectId,
);

Expand All @@ -111,14 +134,40 @@ describe('integration tests', () => {
const owners = await readModel.getAllProjectOwners();

expect(owners).toMatchObject({
[projectId]: [{ name: 'owner' }],
[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,
);

// fetch project owners
const owners = await readModel.getAllProjectOwners();

expect(owners).toMatchObject({
[projectId]: [
{
ownerType: 'group',
name: 'Group Name',
},
],
});
});

test('gets project group owners', async () => {});
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', () => {});

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();

Expand Down
94 changes: 68 additions & 26 deletions src/lib/features/project/project-owners-read-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ import type { Db } from '../../db/db';
import { RoleName, type IProjectWithCount, type IRoleStore } from '../../types';

export type SystemOwner = { ownerType: 'system' };
export type NonSystemProjectOwner =
| {
ownerType: 'user';
name: string;
email?: string;
imageUrl?: string;
}
| {
ownerType: 'group';
name: string;
};

type ProjectOwners = [SystemOwner] | NonSystemProjectOwner[];
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>;

Expand Down Expand Up @@ -43,6 +44,7 @@ export class ProjectOwnersReadModel {
}

async getAllProjectOwners(): Promise<ProjectOwnersDictionary> {
// Query both user and group owners
const T = {
ROLE_USER: 'role_user',
GROUP_ROLE: 'group_role',
Expand All @@ -52,7 +54,7 @@ export class ProjectOwnersReadModel {

const ownerRole = await this.roleStore.getRoleByName(RoleName.OWNER);

const query = this.db
const usersResult = await this.db
.select(
'user.username',
'user.name',
Expand All @@ -66,25 +68,65 @@ export class ProjectOwnersReadModel {
.where('r.id', ownerRole.id)
.join(`${T.USERS} as user`, 'ru.user_id', 'user.id');

const result = await query;
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', ownerRole.id)
.join('groups', 'gr.group_id', 'groups.id');

// Map results into project owners format
const usersDict: Record<string, UserProjectOwner[]> = {};
const groupsDict: Record<string, GroupProjectOwner[]> = {};

const dict = result.reduce((acc, next) => {
const { project } = next;
usersResult.forEach((user) => {
const project = user.project as string;

const userData = {
const data: UserProjectOwner = {
ownerType: 'user',
name: next?.name || next?.username,
email: next?.email,
imageUrl: next?.image_url,
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];
}
});

groupsResult.forEach((group) => {
const project = group.project as string;

const data: GroupProjectOwner = {
ownerType: 'group',
name: group?.name,
};

if (project in acc) {
acc[project].push(userData);
if (project in groupsDict) {
groupsDict[project] = [...groupsDict[project], data];
} else {
acc[project] = [userData];
groupsDict[project] = [data];
}
return acc;
}, {});
});

// Combine user and group owners
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;
}
Expand Down

0 comments on commit 26a9581

Please sign in to comment.