1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00
unleash.unleash/src/lib/services/group-service.ts
Nuno Góis 521cc24a22
feat: add more events in integrations (#4815)
https://linear.app/unleash/issue/2-1253/add-support-for-more-events-in-the-slack-app-integration

Adds support for a lot more events in our integrations. Here is how the
full list looks like:

- ADDON_CONFIG_CREATED
- ADDON_CONFIG_DELETED
- ADDON_CONFIG_UPDATED
- API_TOKEN_CREATED
- API_TOKEN_DELETED
- CHANGE_ADDED
- CHANGE_DISCARDED
- CHANGE_EDITED
- CHANGE_REQUEST_APPLIED
- CHANGE_REQUEST_APPROVAL_ADDED
- CHANGE_REQUEST_APPROVED
- CHANGE_REQUEST_CANCELLED
- CHANGE_REQUEST_CREATED
- CHANGE_REQUEST_DISCARDED
- CHANGE_REQUEST_REJECTED
- CHANGE_REQUEST_SENT_TO_REVIEW
- CONTEXT_FIELD_CREATED
- CONTEXT_FIELD_DELETED
- CONTEXT_FIELD_UPDATED
- FEATURE_ARCHIVED
- FEATURE_CREATED
- FEATURE_DELETED
- FEATURE_ENVIRONMENT_DISABLED
- FEATURE_ENVIRONMENT_ENABLED
- FEATURE_ENVIRONMENT_VARIANTS_UPDATED
- FEATURE_METADATA_UPDATED
- FEATURE_POTENTIALLY_STALE_ON
- FEATURE_PROJECT_CHANGE
- FEATURE_REVIVED
- FEATURE_STALE_OFF
- FEATURE_STALE_ON
- FEATURE_STRATEGY_ADD
- FEATURE_STRATEGY_REMOVE
- FEATURE_STRATEGY_UPDATE
- FEATURE_TAGGED
- FEATURE_UNTAGGED
- GROUP_CREATED
- GROUP_DELETED
- GROUP_UPDATED
- PROJECT_CREATED
- PROJECT_DELETED
- SEGMENT_CREATED
- SEGMENT_DELETED
- SEGMENT_UPDATED
- SERVICE_ACCOUNT_CREATED
- SERVICE_ACCOUNT_DELETED
- SERVICE_ACCOUNT_UPDATED
- USER_CREATED
- USER_DELETED
- USER_UPDATED

I added the events that I thought were relevant based on my own
discretion. Know of any event we should add? Let me know and I'll add it
🙂

For now I only added these events to the new Slack App integration, but
we can add them to the other integrations as well since they are now
supported.

The event formatter was refactored and changed quite a bit in order to
make it easier to maintain and add new events in the future. As a
result, events are now posted with different text. Do we consider this a
breaking change? If so, I can keep the old event formatter around,
create a new one and only use it for the new Slack App integration.

I noticed we don't have good 404 behaviors in the UI for things that are
deleted in the meantime, that's why I avoided some links to specific
resources (like feature strategies, integration configurations, etc),
but we could add them later if we improve this.

This PR also tries to add some consistency to the the way we log events.
2023-09-29 16:11:59 +01:00

252 lines
7.7 KiB
TypeScript

import {
ICreateGroupModel,
IGroup,
IGroupModel,
IGroupModelWithProjectRole,
IGroupProject,
IGroupRole,
IGroupUser,
} from '../types/group';
import { IUnleashConfig, IUnleashStores } from '../types';
import { IGroupStore } from '../types/stores/group-store';
import { Logger } from '../logger';
import BadDataError from '../error/bad-data-error';
import { GROUP_CREATED, GROUP_DELETED, GROUP_UPDATED } from '../types/events';
import NameExistsError from '../error/name-exists-error';
import { IAccountStore } from '../types/stores/account-store';
import { IUser } from '../types/user';
import EventService from './event-service';
export class GroupService {
private groupStore: IGroupStore;
private eventService: EventService;
private accountStore: IAccountStore;
private logger: Logger;
constructor(
stores: Pick<IUnleashStores, 'groupStore' | 'accountStore'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
eventService: EventService,
) {
this.logger = getLogger('service/group-service.js');
this.groupStore = stores.groupStore;
this.eventService = eventService;
this.accountStore = stores.accountStore;
}
async getAll(): Promise<IGroupModel[]> {
const groups = await this.groupStore.getAll();
const allGroupUsers = await this.groupStore.getAllUsersByGroups(
groups.map((g) => g.id),
);
const users = await this.accountStore.getAllWithId(
allGroupUsers.map((u) => u.userId),
);
const groupProjects = await this.groupStore.getGroupProjects(
groups.map((g) => g.id),
);
return groups.map((group) => {
const mappedGroup = this.mapGroupWithUsers(
group,
allGroupUsers,
users,
);
return this.mapGroupWithProjects(groupProjects, mappedGroup);
});
}
mapGroupWithProjects(
groupProjects: IGroupProject[],
group: IGroupModel,
): IGroupModel {
return {
...group,
projects: groupProjects
.filter((project) => project.groupId === group.id)
.map((project) => project.project),
};
}
async getGroup(id: number): Promise<IGroupModel> {
const group = await this.groupStore.get(id);
const groupUsers = await this.groupStore.getAllUsersByGroups([id]);
const users = await this.accountStore.getAllWithId(
groupUsers.map((u) => u.userId),
);
return this.mapGroupWithUsers(group, groupUsers, users);
}
async createGroup(
group: ICreateGroupModel,
userName: string,
): Promise<IGroup> {
await this.validateGroup(group);
const newGroup = await this.groupStore.create(group);
await this.groupStore.addUsersToGroup(
newGroup.id,
group.users,
userName,
);
await this.eventService.storeEvent({
type: GROUP_CREATED,
createdBy: userName,
data: group,
});
return newGroup;
}
async updateGroup(group: IGroupModel, userName: string): Promise<IGroup> {
const preData = await this.groupStore.get(group.id);
await this.validateGroup(group, preData);
const newGroup = await this.groupStore.update(group);
const existingUsers = await this.groupStore.getAllUsersByGroups([
group.id,
]);
const existingUserIds = existingUsers.map((g) => g.userId);
const deletableUsers = existingUsers.filter(
(existingUser) =>
!group.users.some(
(groupUser) => groupUser.user.id === existingUser.userId,
),
);
await this.groupStore.updateGroupUsers(
newGroup.id,
group.users.filter(
(user) => !existingUserIds.includes(user.user.id),
),
deletableUsers,
userName,
);
await this.eventService.storeEvent({
type: GROUP_UPDATED,
createdBy: userName,
data: newGroup,
preData,
});
return newGroup;
}
async getProjectGroups(
projectId: string,
): Promise<IGroupModelWithProjectRole[]> {
const projectGroups = await this.groupStore.getProjectGroups(projectId);
if (projectGroups.length > 0) {
const groups = await this.groupStore.getAllWithId(
projectGroups.map((g) => g.id),
);
const groupUsers = await this.groupStore.getAllUsersByGroups(
groups.map((g) => g.id),
);
const users = await this.accountStore.getAllWithId(
groupUsers.map((u) => u.userId),
);
return groups.flatMap((group) => {
return projectGroups
.filter((gr) => gr.id === group.id)
.map((groupRole) => ({
...this.mapGroupWithUsers(group, groupUsers, users),
...groupRole,
}));
});
}
return [];
}
async deleteGroup(id: number, userName: string): Promise<void> {
const group = await this.groupStore.get(id);
await this.groupStore.delete(id);
await this.eventService.storeEvent({
type: GROUP_DELETED,
createdBy: userName,
preData: group,
});
}
async validateGroup(
group: IGroupModel | ICreateGroupModel,
existingGroup?: IGroup,
): Promise<void> {
if (!group.name) {
throw new BadDataError('Group name cannot be empty');
}
if (!existingGroup || existingGroup.name !== group.name) {
if (await this.groupStore.existsWithName(group.name)) {
throw new NameExistsError('Group name already exists');
}
}
}
async getRolesForProject(projectId: string): Promise<IGroupRole[]> {
return this.groupStore.getProjectGroupRoles(projectId);
}
private mapGroupWithUsers(
group: IGroup,
allGroupUsers: IGroupUser[],
allUsers: IUser[],
): IGroupModel {
const groupUsers = allGroupUsers.filter(
(user) => user.groupId === group.id,
);
const groupUsersId = groupUsers.map((user) => user.userId);
const selectedUsers = allUsers.filter((user) =>
groupUsersId.includes(user.id),
);
const finalUsers = selectedUsers.map((user) => {
const roleUser = groupUsers.find((gu) => gu.userId === user.id);
return {
user: user,
joinedAt: roleUser?.joinedAt,
createdBy: roleUser?.createdBy,
};
});
return { ...group, users: finalUsers };
}
async syncExternalGroups(
userId: number,
externalGroups: string[],
createdBy?: string,
): Promise<void> {
if (Array.isArray(externalGroups)) {
const newGroups = await this.groupStore.getNewGroupsForExternalUser(
userId,
externalGroups,
);
await this.groupStore.addUserToGroups(
userId,
newGroups.map((g) => g.id),
createdBy,
);
const oldGroups = await this.groupStore.getOldGroupsForExternalUser(
userId,
externalGroups,
);
await this.groupStore.deleteUsersFromGroup(oldGroups);
}
}
async getGroupsForUser(userId: number): Promise<IGroup[]> {
return this.groupStore.getGroupsForUser(userId);
}
}