mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: map project owners to projects list (#6928)
- Combining list of projects with owners - Additional tests and checks
This commit is contained in:
parent
44521c1c74
commit
34c1da58cc
@ -4,10 +4,53 @@ import { type IUser, RoleName, type IGroup } from '../../types';
|
|||||||
import { randomId } from '../../util';
|
import { randomId } from '../../util';
|
||||||
import { ProjectOwnersReadModel } from './project-owners-read-model';
|
import { ProjectOwnersReadModel } from './project-owners-read-model';
|
||||||
|
|
||||||
|
const mockProjectWithCounts = (name: string) => ({
|
||||||
|
name,
|
||||||
|
id: name,
|
||||||
|
description: '',
|
||||||
|
featureCount: 0,
|
||||||
|
memberCount: 0,
|
||||||
|
mode: 'open' as const,
|
||||||
|
defaultStickiness: 'default' as const,
|
||||||
|
staleFeatureCount: 0,
|
||||||
|
potentiallyStaleFeatureCount: 0,
|
||||||
|
avgTimeToProduction: 0,
|
||||||
|
});
|
||||||
|
|
||||||
describe('unit tests', () => {
|
describe('unit tests', () => {
|
||||||
test('maps owners to projects', () => {});
|
test('maps owners to projects', () => {
|
||||||
|
const projects = [{ name: 'project1' }, { name: 'project2' }] as any;
|
||||||
|
|
||||||
|
const owners = {
|
||||||
|
project1: [{ ownerType: 'user' as const, name: 'Owner Name' }],
|
||||||
|
project2: [{ ownerType: 'user' as const, name: 'Owner Name' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectsWithOwners = ProjectOwnersReadModel.addOwnerData(
|
||||||
|
projects,
|
||||||
|
owners,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(projectsWithOwners).toMatchObject([
|
||||||
|
{ name: 'project1', owners: [{ name: 'Owner Name' }] },
|
||||||
|
{ name: 'project2', owners: [{ name: 'Owner Name' }] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('returns "system" when a project has no owners', async () => {
|
test('returns "system" when a project has no owners', async () => {
|
||||||
// this is a mapping test; not an integration test
|
const projects = [{ name: 'project1' }, { name: 'project2' }] as any;
|
||||||
|
|
||||||
|
const owners = {};
|
||||||
|
|
||||||
|
const projectsWithOwners = ProjectOwnersReadModel.addOwnerData(
|
||||||
|
projects,
|
||||||
|
owners,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(projectsWithOwners).toMatchObject([
|
||||||
|
{ name: 'project1', owners: [{ ownerType: 'system' }] },
|
||||||
|
{ name: 'project2', owners: [{ ownerType: 'system' }] },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -16,8 +59,10 @@ let readModel: ProjectOwnersReadModel;
|
|||||||
|
|
||||||
let ownerRoleId: number;
|
let ownerRoleId: number;
|
||||||
let owner: IUser;
|
let owner: IUser;
|
||||||
|
let owner2: IUser;
|
||||||
let member: IUser;
|
let member: IUser;
|
||||||
let group: IGroup;
|
let group: IGroup;
|
||||||
|
let group2: IGroup;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('project_owners_read_model_serial', getLogger);
|
db = await dbInit('project_owners_read_model_serial', getLogger);
|
||||||
@ -25,11 +70,17 @@ beforeAll(async () => {
|
|||||||
ownerRoleId = (await db.stores.roleStore.getRoleByName(RoleName.OWNER)).id;
|
ownerRoleId = (await db.stores.roleStore.getRoleByName(RoleName.OWNER)).id;
|
||||||
|
|
||||||
const ownerData = {
|
const ownerData = {
|
||||||
name: 'Owner User',
|
name: 'Owner Name',
|
||||||
username: 'owner',
|
username: 'owner',
|
||||||
email: 'owner@email.com',
|
email: 'owner@email.com',
|
||||||
imageUrl: 'image-url-1',
|
imageUrl: 'image-url-1',
|
||||||
};
|
};
|
||||||
|
const ownerData2 = {
|
||||||
|
name: 'Second Owner Name',
|
||||||
|
username: 'owner2',
|
||||||
|
email: 'owner2@email.com',
|
||||||
|
imageUrl: 'image-url-3',
|
||||||
|
};
|
||||||
const memberData = {
|
const memberData = {
|
||||||
name: 'Member Name',
|
name: 'Member Name',
|
||||||
username: 'member',
|
username: 'member',
|
||||||
@ -40,10 +91,13 @@ beforeAll(async () => {
|
|||||||
// create users
|
// create users
|
||||||
owner = await db.stores.userStore.insert(ownerData);
|
owner = await db.stores.userStore.insert(ownerData);
|
||||||
member = await db.stores.userStore.insert(memberData);
|
member = await db.stores.userStore.insert(memberData);
|
||||||
|
owner2 = await db.stores.userStore.insert(ownerData2);
|
||||||
|
|
||||||
// create groups
|
// create groups
|
||||||
group = await db.stores.groupStore.create({ name: 'Group Name' });
|
group = await db.stores.groupStore.create({ name: 'Group Name' });
|
||||||
await db.stores.groupStore.addUserToGroups(owner.id, [group.id]);
|
await db.stores.groupStore.addUserToGroups(owner.id, [group.id]);
|
||||||
|
group2 = await db.stores.groupStore.create({ name: 'Second Group Name' });
|
||||||
|
await db.stores.groupStore.addUserToGroups(member.id, [group.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -64,6 +118,12 @@ afterEach(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('integration tests', () => {
|
describe('integration tests', () => {
|
||||||
|
test('returns an empty object if there are no projects', async () => {
|
||||||
|
const owners = await readModel.getAllProjectOwners();
|
||||||
|
|
||||||
|
expect(owners).toStrictEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
test('name takes precedence over username', async () => {
|
test('name takes precedence over username', async () => {
|
||||||
const projectId = randomId();
|
const projectId = randomId();
|
||||||
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
||||||
@ -77,7 +137,7 @@ describe('integration tests', () => {
|
|||||||
const owners = await readModel.getAllProjectOwners();
|
const owners = await readModel.getAllProjectOwners();
|
||||||
expect(owners).toMatchObject({
|
expect(owners).toMatchObject({
|
||||||
[projectId]: expect.arrayContaining([
|
[projectId]: expect.arrayContaining([
|
||||||
expect.objectContaining({ name: 'Owner User' }),
|
expect.objectContaining({ name: 'Owner Name' }),
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -98,7 +158,7 @@ describe('integration tests', () => {
|
|||||||
[projectId]: [
|
[projectId]: [
|
||||||
{
|
{
|
||||||
ownerType: 'user',
|
ownerType: 'user',
|
||||||
name: 'Owner User',
|
name: 'Owner Name',
|
||||||
email: 'owner@email.com',
|
email: 'owner@email.com',
|
||||||
imageUrl: 'image-url-1',
|
imageUrl: 'image-url-1',
|
||||||
},
|
},
|
||||||
@ -128,7 +188,7 @@ describe('integration tests', () => {
|
|||||||
const owners = await readModel.getAllProjectOwners();
|
const owners = await readModel.getAllProjectOwners();
|
||||||
|
|
||||||
expect(owners).toMatchObject({
|
expect(owners).toMatchObject({
|
||||||
[projectId]: [{ name: 'Owner User' }],
|
[projectId]: [{ name: 'Owner Name' }],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -155,21 +215,134 @@ describe('integration tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('users are listed before groups', async () => {});
|
test('users are listed before groups', async () => {
|
||||||
|
const projectId = randomId();
|
||||||
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
||||||
|
|
||||||
test('owners (users and groups) are sorted by when they were added; oldest first', async () => {});
|
await db.stores.accessStore.addGroupToRole(
|
||||||
|
group.id,
|
||||||
|
ownerRoleId,
|
||||||
|
'',
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
|
||||||
test('returns the system owner for the default project', async () => {});
|
await db.stores.accessStore.addUserToRole(
|
||||||
|
owner.id,
|
||||||
|
ownerRoleId,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
|
||||||
test('returns an empty list if there are no projects', async () => {
|
|
||||||
const owners = await readModel.getAllProjectOwners();
|
const owners = await readModel.getAllProjectOwners();
|
||||||
|
|
||||||
expect(owners).toStrictEqual({});
|
expect(owners).toMatchObject({
|
||||||
|
[projectId]: [
|
||||||
|
{
|
||||||
|
email: 'owner@email.com',
|
||||||
|
imageUrl: 'image-url-1',
|
||||||
|
name: 'Owner Name',
|
||||||
|
ownerType: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Group Name',
|
||||||
|
ownerType: 'group',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('enriches fully', async () => {
|
test('owners (users and groups) are sorted by when they were added; oldest first', async () => {
|
||||||
const owners = await readModel.enrichWithOwners([]);
|
const projectId = randomId();
|
||||||
|
await db.stores.projectStore.create({ id: projectId, name: projectId });
|
||||||
|
|
||||||
expect(owners).toStrictEqual([]);
|
// Raw query in order to set the created_at date
|
||||||
|
await db.rawDatabase('role_user').insert({
|
||||||
|
user_id: owner2.id,
|
||||||
|
role_id: ownerRoleId,
|
||||||
|
project: projectId,
|
||||||
|
created_at: new Date('2024-01-01T00:00:00.000Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Raw query in order to set the created_at date
|
||||||
|
await db.rawDatabase('group_role').insert({
|
||||||
|
group_id: group2.id,
|
||||||
|
role_id: ownerRoleId,
|
||||||
|
project: projectId,
|
||||||
|
created_at: new Date('2024-01-01T00:00:00.000Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.stores.accessStore.addGroupToRole(
|
||||||
|
group.id,
|
||||||
|
ownerRoleId,
|
||||||
|
'',
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.stores.accessStore.addUserToRole(
|
||||||
|
owner.id,
|
||||||
|
ownerRoleId,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const owners = await readModel.getAllProjectOwners();
|
||||||
|
|
||||||
|
expect(owners).toMatchObject({
|
||||||
|
[projectId]: [
|
||||||
|
{
|
||||||
|
email: 'owner2@email.com',
|
||||||
|
imageUrl: 'image-url-3',
|
||||||
|
name: 'Second Owner Name',
|
||||||
|
ownerType: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'owner@email.com',
|
||||||
|
imageUrl: 'image-url-1',
|
||||||
|
name: 'Owner Name',
|
||||||
|
ownerType: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Second Group Name',
|
||||||
|
ownerType: 'group',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Group Name',
|
||||||
|
ownerType: 'group',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not modify an empty array', async () => {
|
||||||
|
const projectsWithOwners = await readModel.addOwners([]);
|
||||||
|
|
||||||
|
expect(projectsWithOwners).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds system owner when no owners are found', async () => {
|
||||||
|
const projectIdA = randomId();
|
||||||
|
const projectIdB = randomId();
|
||||||
|
await db.stores.projectStore.create({
|
||||||
|
id: projectIdA,
|
||||||
|
name: projectIdA,
|
||||||
|
});
|
||||||
|
await db.stores.projectStore.create({
|
||||||
|
id: projectIdB,
|
||||||
|
name: projectIdB,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.stores.accessStore.addUserToRole(
|
||||||
|
owner.id,
|
||||||
|
ownerRoleId,
|
||||||
|
projectIdB,
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectsWithOwners = await readModel.addOwners([
|
||||||
|
mockProjectWithCounts(projectIdA),
|
||||||
|
mockProjectWithCounts(projectIdB),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(projectsWithOwners).toMatchObject([
|
||||||
|
{ name: projectIdA, owners: [{ ownerType: 'system' }] },
|
||||||
|
{ name: projectIdB, owners: [{ ownerType: 'user' }] },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -38,15 +38,14 @@ export class ProjectOwnersReadModel {
|
|||||||
this.roleStore = roleStore;
|
this.roleStore = roleStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
addOwnerData(
|
static addOwnerData(
|
||||||
projects: IProjectWithCount[],
|
projects: IProjectWithCount[],
|
||||||
owners: ProjectOwnersDictionary,
|
owners: ProjectOwnersDictionary,
|
||||||
): IProjectWithCountAndOwners[] {
|
): IProjectWithCountAndOwners[] {
|
||||||
// const projectsWithOwners = projects.map((p) => ({
|
return projects.map((project) => ({
|
||||||
// ...p,
|
...project,
|
||||||
// owners: projectOwners[p.id] || [],
|
owners: owners[project.name] || [{ ownerType: 'system' }],
|
||||||
// }));
|
}));
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAllProjectUsersByRole(
|
private async getAllProjectUsersByRole(
|
||||||
@ -62,6 +61,7 @@ export class ProjectOwnersReadModel {
|
|||||||
'ru.project',
|
'ru.project',
|
||||||
)
|
)
|
||||||
.from(`${T.ROLE_USER} as ru`)
|
.from(`${T.ROLE_USER} as ru`)
|
||||||
|
.orderBy('ru.created_at', 'asc')
|
||||||
.join(`${T.ROLES} as r`, 'ru.role_id', 'r.id')
|
.join(`${T.ROLES} as r`, 'ru.role_id', 'r.id')
|
||||||
.where('r.id', roleId)
|
.where('r.id', roleId)
|
||||||
.join(`${T.USERS} as user`, 'ru.user_id', 'user.id');
|
.join(`${T.USERS} as user`, 'ru.user_id', 'user.id');
|
||||||
@ -93,6 +93,7 @@ export class ProjectOwnersReadModel {
|
|||||||
const groupsResult = await this.db
|
const groupsResult = await this.db
|
||||||
.select('groups.name', 'gr.created_at', 'gr.project')
|
.select('groups.name', 'gr.created_at', 'gr.project')
|
||||||
.from(`${T.GROUP_ROLE} as gr`)
|
.from(`${T.GROUP_ROLE} as gr`)
|
||||||
|
.orderBy('gr.created_at', 'asc')
|
||||||
.join(`${T.ROLES} as r`, 'gr.role_id', 'r.id')
|
.join(`${T.ROLES} as r`, 'gr.role_id', 'r.id')
|
||||||
.where('r.id', roleId)
|
.where('r.id', roleId)
|
||||||
.join('groups', 'gr.group_id', 'groups.id');
|
.join('groups', 'gr.group_id', 'groups.id');
|
||||||
@ -122,30 +123,27 @@ export class ProjectOwnersReadModel {
|
|||||||
const usersDict = await this.getAllProjectUsersByRole(ownerRole.id);
|
const usersDict = await this.getAllProjectUsersByRole(ownerRole.id);
|
||||||
const groupsDict = await this.getAllProjectGroupsByRole(ownerRole.id);
|
const groupsDict = await this.getAllProjectGroupsByRole(ownerRole.id);
|
||||||
|
|
||||||
const projects = [
|
const dict: Record<
|
||||||
...new Set([...Object.keys(usersDict), ...Object.keys(groupsDict)]),
|
string,
|
||||||
];
|
Array<UserProjectOwner | GroupProjectOwner>
|
||||||
|
> = usersDict;
|
||||||
|
|
||||||
const dict = Object.fromEntries(
|
Object.keys(groupsDict).forEach((project) => {
|
||||||
projects.map((project) => {
|
if (project in dict) {
|
||||||
return [
|
dict[project] = dict[project].concat(groupsDict[project]);
|
||||||
project,
|
} else {
|
||||||
[
|
dict[project] = groupsDict[project];
|
||||||
...(usersDict[project] || []),
|
}
|
||||||
...(groupsDict[project] || []),
|
});
|
||||||
],
|
|
||||||
];
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
async enrichWithOwners(
|
async addOwners(
|
||||||
projects: IProjectWithCount[],
|
projects: IProjectWithCount[],
|
||||||
): Promise<IProjectWithCountAndOwners[]> {
|
): Promise<IProjectWithCountAndOwners[]> {
|
||||||
const owners = await this.getAllProjectOwners();
|
const owners = await this.getAllProjectOwners();
|
||||||
|
|
||||||
return this.addOwnerData(projects, owners);
|
return ProjectOwnersReadModel.addOwnerData(projects, owners);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user