mirror of
https://github.com/Unleash/unleash.git
synced 2024-11-01 19:07:38 +01:00
1ccbbfeb57
<!-- Thanks for creating a PR! To make it easier for reviewers and everyone else to understand what your changes relate to, please add some relevant content to the headings below. Feel free to ignore or delete sections that you don't think are relevant. Thank you! ❤️ --> Adds default strategy to project environment link table ## About the changes <!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. --> <!-- Does it close an issue? Multiple? --> Closes # [1-876](https://linear.app/unleash/issue/1-876/default-strategy-backend) <!-- (For internal contributors): Does it relate to an issue on public roadmap? --> <!-- Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: # --> ### Important files <!-- PRs can contain a lot of changes, but not all changes are equally important. Where should a reviewer start looking to get an overview of the changes? Are any files particularly important? --> ## Discussion points <!-- Anything about the PR you'd like to discuss before it gets merged? Got any questions or doubts? --> --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
852 lines
25 KiB
TypeScript
852 lines
25 KiB
TypeScript
import { subDays } from 'date-fns';
|
|
import { ValidationError } from 'joi';
|
|
import User, { IUser } from '../types/user';
|
|
import { AccessService } from './access-service';
|
|
import NameExistsError from '../error/name-exists-error';
|
|
import InvalidOperationError from '../error/invalid-operation-error';
|
|
import { nameType } from '../routes/util';
|
|
import { projectSchema } from './project-schema';
|
|
import NotFoundError from '../error/notfound-error';
|
|
import {
|
|
DEFAULT_PROJECT,
|
|
FeatureToggle,
|
|
IAccountStore,
|
|
IEnvironmentStore,
|
|
IEventStore,
|
|
IFeatureEnvironmentStore,
|
|
IFeatureToggleStore,
|
|
IFeatureTypeStore,
|
|
IProject,
|
|
IProjectOverview,
|
|
IProjectWithCount,
|
|
IUnleashConfig,
|
|
IUnleashStores,
|
|
IUserWithRole,
|
|
MOVE_FEATURE_TOGGLE,
|
|
PROJECT_CREATED,
|
|
PROJECT_DELETED,
|
|
PROJECT_UPDATED,
|
|
ProjectGroupAddedEvent,
|
|
ProjectGroupRemovedEvent,
|
|
ProjectGroupUpdateRoleEvent,
|
|
ProjectMode,
|
|
ProjectUserAddedEvent,
|
|
ProjectUserRemovedEvent,
|
|
ProjectUserUpdateRoleEvent,
|
|
RoleName,
|
|
IFlagResolver,
|
|
} from '../types';
|
|
import {
|
|
IProjectQuery,
|
|
IProjectSettings,
|
|
IProjectStore,
|
|
} from '../types/stores/project-store';
|
|
import {
|
|
IProjectAccessModel,
|
|
IRoleDescriptor,
|
|
} from '../types/stores/access-store';
|
|
import FeatureToggleService from './feature-toggle-service';
|
|
import NoAccessError from '../error/no-access-error';
|
|
import IncompatibleProjectError from '../error/incompatible-project-error';
|
|
import { IFeatureTagStore } from 'lib/types/stores/feature-tag-store';
|
|
import ProjectWithoutOwnerError from '../error/project-without-owner-error';
|
|
import { arraysHaveSameItems } from '../util';
|
|
import { GroupService } from './group-service';
|
|
import { IGroupModelWithProjectRole, IGroupRole } from 'lib/types/group';
|
|
import { FavoritesService } from './favorites-service';
|
|
import { calculateAverageTimeToProd } from '../features/feature-toggle/time-to-production/time-to-production';
|
|
import { IProjectStatsStore } from 'lib/types/stores/project-stats-store-type';
|
|
import { uniqueByKey } from '../util/unique';
|
|
|
|
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
|
|
|
|
export interface AccessWithRoles {
|
|
users: IUserWithRole[];
|
|
roles: IRoleDescriptor[];
|
|
groups: IGroupModelWithProjectRole[];
|
|
}
|
|
|
|
type Days = number;
|
|
type Count = number;
|
|
|
|
export interface IProjectStats {
|
|
avgTimeToProdCurrentWindow: Days;
|
|
createdCurrentWindow: Count;
|
|
createdPastWindow: Count;
|
|
archivedCurrentWindow: Count;
|
|
archivedPastWindow: Count;
|
|
projectActivityCurrentWindow: Count;
|
|
projectActivityPastWindow: Count;
|
|
projectMembersAddedCurrentWindow: Count;
|
|
}
|
|
|
|
interface ICalculateStatus {
|
|
projectId: string;
|
|
updates: IProjectStats;
|
|
}
|
|
|
|
export default class ProjectService {
|
|
private store: IProjectStore;
|
|
|
|
private accessService: AccessService;
|
|
|
|
private eventStore: IEventStore;
|
|
|
|
private featureToggleStore: IFeatureToggleStore;
|
|
|
|
private featureTypeStore: IFeatureTypeStore;
|
|
|
|
private featureEnvironmentStore: IFeatureEnvironmentStore;
|
|
|
|
private environmentStore: IEnvironmentStore;
|
|
|
|
private groupService: GroupService;
|
|
|
|
private logger: any;
|
|
|
|
private featureToggleService: FeatureToggleService;
|
|
|
|
private tagStore: IFeatureTagStore;
|
|
|
|
private accountStore: IAccountStore;
|
|
|
|
private favoritesService: FavoritesService;
|
|
|
|
private projectStatsStore: IProjectStatsStore;
|
|
|
|
private flagResolver: IFlagResolver;
|
|
|
|
constructor(
|
|
{
|
|
projectStore,
|
|
eventStore,
|
|
featureToggleStore,
|
|
featureTypeStore,
|
|
environmentStore,
|
|
featureEnvironmentStore,
|
|
featureTagStore,
|
|
accountStore,
|
|
projectStatsStore,
|
|
}: Pick<
|
|
IUnleashStores,
|
|
| 'projectStore'
|
|
| 'eventStore'
|
|
| 'featureToggleStore'
|
|
| 'featureTypeStore'
|
|
| 'environmentStore'
|
|
| 'featureEnvironmentStore'
|
|
| 'featureTagStore'
|
|
| 'accountStore'
|
|
| 'projectStatsStore'
|
|
>,
|
|
config: IUnleashConfig,
|
|
accessService: AccessService,
|
|
featureToggleService: FeatureToggleService,
|
|
groupService: GroupService,
|
|
favoriteService: FavoritesService,
|
|
) {
|
|
this.store = projectStore;
|
|
this.environmentStore = environmentStore;
|
|
this.featureEnvironmentStore = featureEnvironmentStore;
|
|
this.accessService = accessService;
|
|
this.eventStore = eventStore;
|
|
this.featureToggleStore = featureToggleStore;
|
|
this.featureTypeStore = featureTypeStore;
|
|
this.featureToggleService = featureToggleService;
|
|
this.favoritesService = favoriteService;
|
|
this.tagStore = featureTagStore;
|
|
this.accountStore = accountStore;
|
|
this.groupService = groupService;
|
|
this.projectStatsStore = projectStatsStore;
|
|
this.logger = config.getLogger('services/project-service.js');
|
|
this.flagResolver = config.flagResolver;
|
|
}
|
|
|
|
async getProjects(
|
|
query?: IProjectQuery,
|
|
userId?: number,
|
|
): Promise<IProjectWithCount[]> {
|
|
return this.store.getProjectsWithCounts(query, userId);
|
|
}
|
|
|
|
async getProject(id: string): Promise<IProject> {
|
|
return this.store.get(id);
|
|
}
|
|
|
|
async createProject(
|
|
newProject: Pick<
|
|
IProject,
|
|
'id' | 'name' | 'mode' | 'defaultStickiness'
|
|
>,
|
|
user: IUser,
|
|
): Promise<IProject> {
|
|
const data = await projectSchema.validateAsync(newProject);
|
|
await this.validateUniqueId(data.id);
|
|
|
|
await this.store.create(data);
|
|
|
|
const enabledEnvironments = await this.environmentStore.getAll({
|
|
enabled: true,
|
|
});
|
|
|
|
// TODO: Only if enabled!
|
|
await Promise.all(
|
|
enabledEnvironments.map(async (e) => {
|
|
await this.featureEnvironmentStore.connectProject(
|
|
e.name,
|
|
data.id,
|
|
);
|
|
}),
|
|
);
|
|
|
|
await this.accessService.createDefaultProjectRoles(user, data.id);
|
|
|
|
await this.eventStore.store({
|
|
type: PROJECT_CREATED,
|
|
createdBy: getCreatedBy(user),
|
|
data,
|
|
project: newProject.id,
|
|
});
|
|
|
|
return data;
|
|
}
|
|
|
|
async updateProject(updatedProject: IProject, user: User): Promise<void> {
|
|
const preData = await this.store.get(updatedProject.id);
|
|
const project = await projectSchema.validateAsync(updatedProject);
|
|
|
|
await this.store.update(project);
|
|
|
|
await this.eventStore.store({
|
|
type: PROJECT_UPDATED,
|
|
project: project.id,
|
|
createdBy: getCreatedBy(user),
|
|
data: project,
|
|
preData,
|
|
});
|
|
}
|
|
|
|
async checkProjectsCompatibility(
|
|
feature: FeatureToggle,
|
|
newProjectId: string,
|
|
): Promise<boolean> {
|
|
const featureEnvs = await this.featureEnvironmentStore.getAll({
|
|
feature_name: feature.name,
|
|
});
|
|
const newEnvs = await this.store.getEnvironmentsForProject(
|
|
newProjectId,
|
|
);
|
|
return arraysHaveSameItems(
|
|
featureEnvs.map((env) => env.environment),
|
|
newEnvs.map((projectEnv) => projectEnv.environment),
|
|
);
|
|
}
|
|
|
|
async addEnvironmentToProject(
|
|
project: string,
|
|
environment: string,
|
|
): Promise<void> {
|
|
await this.store.addEnvironmentToProject(project, environment);
|
|
}
|
|
|
|
async changeProject(
|
|
newProjectId: string,
|
|
featureName: string,
|
|
user: User,
|
|
currentProjectId: string,
|
|
): Promise<any> {
|
|
const feature = await this.featureToggleStore.get(featureName);
|
|
|
|
if (feature.project !== currentProjectId) {
|
|
throw new NoAccessError(MOVE_FEATURE_TOGGLE);
|
|
}
|
|
const project = await this.getProject(newProjectId);
|
|
|
|
if (!project) {
|
|
throw new NotFoundError(`Project ${newProjectId} not found`);
|
|
}
|
|
|
|
const authorized = await this.accessService.hasPermission(
|
|
user,
|
|
MOVE_FEATURE_TOGGLE,
|
|
newProjectId,
|
|
);
|
|
|
|
if (!authorized) {
|
|
throw new NoAccessError(MOVE_FEATURE_TOGGLE);
|
|
}
|
|
|
|
const isCompatibleWithTargetProject =
|
|
await this.checkProjectsCompatibility(feature, newProjectId);
|
|
if (!isCompatibleWithTargetProject) {
|
|
throw new IncompatibleProjectError(newProjectId);
|
|
}
|
|
const updatedFeature = await this.featureToggleService.changeProject(
|
|
featureName,
|
|
newProjectId,
|
|
getCreatedBy(user),
|
|
);
|
|
await this.featureToggleService.updateFeatureStrategyProject(
|
|
featureName,
|
|
newProjectId,
|
|
);
|
|
|
|
return updatedFeature;
|
|
}
|
|
|
|
async deleteProject(id: string, user: User): Promise<void> {
|
|
if (id === DEFAULT_PROJECT) {
|
|
throw new InvalidOperationError(
|
|
'You can not delete the default project!',
|
|
);
|
|
}
|
|
|
|
const toggles = await this.featureToggleStore.getAll({
|
|
project: id,
|
|
archived: false,
|
|
});
|
|
|
|
if (toggles.length > 0) {
|
|
throw new InvalidOperationError(
|
|
'You can not delete a project with active feature toggles',
|
|
);
|
|
}
|
|
|
|
await this.store.delete(id);
|
|
|
|
await this.eventStore.store({
|
|
type: PROJECT_DELETED,
|
|
createdBy: getCreatedBy(user),
|
|
project: id,
|
|
});
|
|
|
|
await this.accessService.removeDefaultProjectRoles(user, id);
|
|
}
|
|
|
|
async validateId(id: string): Promise<boolean> {
|
|
await nameType.validateAsync(id);
|
|
await this.validateUniqueId(id);
|
|
return true;
|
|
}
|
|
|
|
async validateUniqueId(id: string): Promise<void> {
|
|
const exists = await this.store.hasProject(id);
|
|
if (exists) {
|
|
throw new NameExistsError('A project with this id already exists.');
|
|
}
|
|
}
|
|
|
|
// RBAC methods
|
|
async getAccessToProject(projectId: string): Promise<AccessWithRoles> {
|
|
const [roles, users, groups] =
|
|
await this.accessService.getProjectRoleAccess(projectId);
|
|
|
|
return {
|
|
roles,
|
|
users,
|
|
groups,
|
|
};
|
|
}
|
|
|
|
async addUser(
|
|
projectId: string,
|
|
roleId: number,
|
|
userId: number,
|
|
createdBy: string,
|
|
): Promise<void> {
|
|
const [roles, users] = await this.accessService.getProjectRoleAccess(
|
|
projectId,
|
|
);
|
|
const user = await this.accountStore.get(userId);
|
|
|
|
const role = roles.find((r) => r.id === roleId);
|
|
if (!role) {
|
|
throw new NotFoundError(
|
|
`Could not find roleId=${roleId} on project=${projectId}`,
|
|
);
|
|
}
|
|
|
|
const alreadyHasAccess = users.some((u) => u.id === userId);
|
|
if (alreadyHasAccess) {
|
|
throw new Error(`User already has access to project=${projectId}`);
|
|
}
|
|
|
|
await this.accessService.addUserToRole(userId, role.id, projectId);
|
|
|
|
await this.eventStore.store(
|
|
new ProjectUserAddedEvent({
|
|
project: projectId,
|
|
createdBy: createdBy || 'system-user',
|
|
data: {
|
|
roleId,
|
|
userId,
|
|
roleName: role.name,
|
|
email: user.email,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
async removeUser(
|
|
projectId: string,
|
|
roleId: number,
|
|
userId: number,
|
|
createdBy: string,
|
|
): Promise<void> {
|
|
const role = await this.findProjectRole(projectId, roleId);
|
|
|
|
await this.validateAtLeastOneOwner(projectId, role);
|
|
|
|
await this.accessService.removeUserFromRole(userId, role.id, projectId);
|
|
|
|
const user = await this.accountStore.get(userId);
|
|
|
|
await this.eventStore.store(
|
|
new ProjectUserRemovedEvent({
|
|
project: projectId,
|
|
createdBy,
|
|
preData: {
|
|
roleId,
|
|
userId,
|
|
roleName: role.name,
|
|
email: user.email,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
async addGroup(
|
|
projectId: string,
|
|
roleId: number,
|
|
groupId: number,
|
|
modifiedBy: string,
|
|
): Promise<void> {
|
|
const role = await this.accessService.getRole(roleId);
|
|
const group = await this.groupService.getGroup(groupId);
|
|
const project = await this.getProject(projectId);
|
|
if (group.id == null)
|
|
throw new ValidationError(
|
|
'Unexpected empty group id',
|
|
[],
|
|
undefined,
|
|
);
|
|
|
|
await this.accessService.addGroupToRole(
|
|
group.id,
|
|
role.id,
|
|
modifiedBy,
|
|
project.id,
|
|
);
|
|
|
|
await this.eventStore.store(
|
|
new ProjectGroupAddedEvent({
|
|
project: project.id,
|
|
createdBy: modifiedBy,
|
|
data: {
|
|
groupId: group.id,
|
|
projectId: project.id,
|
|
roleName: role.name,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
async removeGroup(
|
|
projectId: string,
|
|
roleId: number,
|
|
groupId: number,
|
|
modifiedBy: string,
|
|
): Promise<void> {
|
|
const group = await this.groupService.getGroup(groupId);
|
|
const role = await this.accessService.getRole(roleId);
|
|
const project = await this.getProject(projectId);
|
|
if (group.id == null)
|
|
throw new ValidationError(
|
|
'Unexpected empty group id',
|
|
[],
|
|
undefined,
|
|
);
|
|
|
|
await this.accessService.removeGroupFromRole(
|
|
group.id,
|
|
role.id,
|
|
project.id,
|
|
);
|
|
|
|
await this.eventStore.store(
|
|
new ProjectGroupRemovedEvent({
|
|
project: projectId,
|
|
createdBy: modifiedBy,
|
|
preData: {
|
|
groupId: group.id,
|
|
projectId: project.id,
|
|
roleName: role.name,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
async addAccess(
|
|
projectId: string,
|
|
roleId: number,
|
|
usersAndGroups: IProjectAccessModel,
|
|
createdBy: string,
|
|
): Promise<void> {
|
|
return this.accessService.addAccessToProject(
|
|
usersAndGroups.users,
|
|
usersAndGroups.groups,
|
|
projectId,
|
|
roleId,
|
|
createdBy,
|
|
);
|
|
}
|
|
|
|
async findProjectGroupRole(
|
|
projectId: string,
|
|
roleId: number,
|
|
): Promise<IGroupRole> {
|
|
const roles = await this.groupService.getRolesForProject(projectId);
|
|
const role = roles.find((r) => r.roleId === roleId);
|
|
if (!role) {
|
|
throw new NotFoundError(
|
|
`Couldn't find roleId=${roleId} on project=${projectId}`,
|
|
);
|
|
}
|
|
return role;
|
|
}
|
|
|
|
async findProjectRole(
|
|
projectId: string,
|
|
roleId: number,
|
|
): Promise<IRoleDescriptor> {
|
|
const roles = await this.accessService.getRolesForProject(projectId);
|
|
const role = roles.find((r) => r.id === roleId);
|
|
if (!role) {
|
|
throw new NotFoundError(
|
|
`Couldn't find roleId=${roleId} on project=${projectId}`,
|
|
);
|
|
}
|
|
return role;
|
|
}
|
|
|
|
async validateAtLeastOneOwner(
|
|
projectId: string,
|
|
currentRole: IRoleDescriptor,
|
|
): Promise<void> {
|
|
if (currentRole.name === RoleName.OWNER) {
|
|
const users = await this.accessService.getProjectUsersForRole(
|
|
currentRole.id,
|
|
projectId,
|
|
);
|
|
const groups = await this.groupService.getProjectGroups(projectId);
|
|
const roleGroups = groups.filter((g) => g.roleId == currentRole.id);
|
|
if (users.length + roleGroups.length < 2) {
|
|
throw new ProjectWithoutOwnerError();
|
|
}
|
|
}
|
|
}
|
|
|
|
async changeRole(
|
|
projectId: string,
|
|
roleId: number,
|
|
userId: number,
|
|
createdBy: string,
|
|
): Promise<void> {
|
|
const usersWithRoles = await this.getAccessToProject(projectId);
|
|
const user = usersWithRoles.users.find((u) => u.id === userId);
|
|
if (!user)
|
|
throw new ValidationError('Unexpected empty user', [], undefined);
|
|
|
|
const currentRole = usersWithRoles.roles.find(
|
|
(r) => r.id === user.roleId,
|
|
);
|
|
if (!currentRole)
|
|
throw new ValidationError(
|
|
'Unexpected empty current role',
|
|
[],
|
|
undefined,
|
|
);
|
|
|
|
if (currentRole.id === roleId) {
|
|
// Nothing to do....
|
|
return;
|
|
}
|
|
|
|
await this.validateAtLeastOneOwner(projectId, currentRole);
|
|
|
|
await this.accessService.updateUserProjectRole(
|
|
userId,
|
|
roleId,
|
|
projectId,
|
|
);
|
|
const role = await this.findProjectRole(projectId, roleId);
|
|
|
|
await this.eventStore.store(
|
|
new ProjectUserUpdateRoleEvent({
|
|
project: projectId,
|
|
createdBy,
|
|
preData: {
|
|
userId,
|
|
roleId: currentRole.id,
|
|
roleName: currentRole.name,
|
|
email: user.email,
|
|
},
|
|
data: {
|
|
userId,
|
|
roleId,
|
|
roleName: role.name,
|
|
email: user.email,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
async changeGroupRole(
|
|
projectId: string,
|
|
roleId: number,
|
|
userId: number,
|
|
createdBy: string,
|
|
): Promise<void> {
|
|
const usersWithRoles = await this.getAccessToProject(projectId);
|
|
const user = usersWithRoles.groups.find((u) => u.id === userId);
|
|
if (!user)
|
|
throw new ValidationError('Unexpected empty user', [], undefined);
|
|
const currentRole = usersWithRoles.roles.find(
|
|
(r) => r.id === user.roleId,
|
|
);
|
|
if (!currentRole)
|
|
throw new ValidationError(
|
|
'Unexpected empty current role',
|
|
[],
|
|
undefined,
|
|
);
|
|
|
|
if (currentRole.id === roleId) {
|
|
// Nothing to do....
|
|
return;
|
|
}
|
|
|
|
await this.validateAtLeastOneOwner(projectId, currentRole);
|
|
|
|
await this.accessService.updateGroupProjectRole(
|
|
userId,
|
|
roleId,
|
|
projectId,
|
|
);
|
|
const role = await this.findProjectGroupRole(projectId, roleId);
|
|
|
|
await this.eventStore.store(
|
|
new ProjectGroupUpdateRoleEvent({
|
|
project: projectId,
|
|
createdBy,
|
|
preData: {
|
|
userId,
|
|
roleId: currentRole.id,
|
|
roleName: currentRole.name,
|
|
},
|
|
data: {
|
|
userId,
|
|
roleId,
|
|
roleName: role.name,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
async getMembers(projectId: string): Promise<number> {
|
|
return this.store.getMembersCountByProject(projectId);
|
|
}
|
|
|
|
async getProjectUsers(
|
|
projectId: string,
|
|
): Promise<Array<Pick<IUser, 'id' | 'email' | 'username'>>> {
|
|
const [, users, groups] = await this.accessService.getProjectRoleAccess(
|
|
projectId,
|
|
);
|
|
const actualUsers = users.map((user) => ({
|
|
id: user.id,
|
|
email: user.email,
|
|
username: user.username,
|
|
}));
|
|
const actualGroupUsers = groups
|
|
.flatMap((group) => group.users)
|
|
.map((user) => user.user)
|
|
.map((user) => ({
|
|
id: user.id,
|
|
email: user.email,
|
|
username: user.username,
|
|
}));
|
|
return uniqueByKey([...actualUsers, ...actualGroupUsers], 'id');
|
|
}
|
|
|
|
async isProjectUser(userId: number, projectId: string): Promise<boolean> {
|
|
const users = await this.getProjectUsers(projectId);
|
|
return Boolean(users.find((user) => user.id === userId));
|
|
}
|
|
|
|
async getProjectsByUser(userId: number): Promise<string[]> {
|
|
return this.store.getProjectsByUser(userId);
|
|
}
|
|
|
|
async statusJob(): Promise<void> {
|
|
const projects = await this.store.getAll();
|
|
|
|
const statusUpdates = await Promise.all(
|
|
projects.map((project) => this.getStatusUpdates(project.id)),
|
|
);
|
|
|
|
await Promise.all(
|
|
statusUpdates.map((statusUpdate) => {
|
|
return this.projectStatsStore.updateProjectStats(
|
|
statusUpdate.projectId,
|
|
statusUpdate.updates,
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
async getStatusUpdates(projectId: string): Promise<ICalculateStatus> {
|
|
const dateMinusThirtyDays = subDays(new Date(), 30).toISOString();
|
|
const dateMinusSixtyDays = subDays(new Date(), 60).toISOString();
|
|
|
|
const [
|
|
createdCurrentWindow,
|
|
createdPastWindow,
|
|
archivedCurrentWindow,
|
|
archivedPastWindow,
|
|
] = await Promise.all([
|
|
await this.featureToggleStore.countByDate({
|
|
project: projectId,
|
|
dateAccessor: 'created_at',
|
|
date: dateMinusThirtyDays,
|
|
}),
|
|
await this.featureToggleStore.countByDate({
|
|
project: projectId,
|
|
dateAccessor: 'created_at',
|
|
range: [dateMinusSixtyDays, dateMinusThirtyDays],
|
|
}),
|
|
await this.featureToggleStore.countByDate({
|
|
project: projectId,
|
|
archived: true,
|
|
dateAccessor: 'archived_at',
|
|
date: dateMinusThirtyDays,
|
|
}),
|
|
await this.featureToggleStore.countByDate({
|
|
project: projectId,
|
|
archived: true,
|
|
dateAccessor: 'archived_at',
|
|
range: [dateMinusSixtyDays, dateMinusThirtyDays],
|
|
}),
|
|
]);
|
|
|
|
const [projectActivityCurrentWindow, projectActivityPastWindow] =
|
|
await Promise.all([
|
|
this.eventStore.queryCount([
|
|
{ op: 'where', parameters: { project: projectId } },
|
|
{
|
|
op: 'beforeDate',
|
|
parameters: {
|
|
dateAccessor: 'created_at',
|
|
date: dateMinusThirtyDays,
|
|
},
|
|
},
|
|
]),
|
|
this.eventStore.queryCount([
|
|
{ op: 'where', parameters: { project: projectId } },
|
|
{
|
|
op: 'betweenDate',
|
|
parameters: {
|
|
dateAccessor: 'created_at',
|
|
range: [dateMinusSixtyDays, dateMinusThirtyDays],
|
|
},
|
|
},
|
|
]),
|
|
]);
|
|
|
|
const avgTimeToProdCurrentWindow = calculateAverageTimeToProd(
|
|
await this.projectStatsStore.getTimeToProdDates(projectId),
|
|
);
|
|
|
|
const projectMembersAddedCurrentWindow =
|
|
await this.store.getMembersCountByProjectAfterDate(
|
|
projectId,
|
|
dateMinusThirtyDays,
|
|
);
|
|
|
|
return {
|
|
projectId,
|
|
updates: {
|
|
avgTimeToProdCurrentWindow,
|
|
createdCurrentWindow,
|
|
createdPastWindow,
|
|
archivedCurrentWindow,
|
|
archivedPastWindow,
|
|
projectActivityCurrentWindow,
|
|
projectActivityPastWindow,
|
|
projectMembersAddedCurrentWindow,
|
|
},
|
|
};
|
|
}
|
|
|
|
async getProjectOverview(
|
|
projectId: string,
|
|
archived: boolean = false,
|
|
userId?: number,
|
|
): Promise<IProjectOverview> {
|
|
const [
|
|
project,
|
|
environments,
|
|
features,
|
|
members,
|
|
favorite,
|
|
projectStats,
|
|
] = await Promise.all([
|
|
this.store.get(projectId),
|
|
this.store.getEnvironmentsForProject(projectId),
|
|
this.featureToggleService.getFeatureOverview({
|
|
projectId,
|
|
archived,
|
|
userId,
|
|
}),
|
|
this.store.getMembersCountByProject(projectId),
|
|
userId
|
|
? this.favoritesService.isFavoriteProject({
|
|
project: projectId,
|
|
userId,
|
|
})
|
|
: Promise.resolve(false),
|
|
this.projectStatsStore.getProjectStats(projectId),
|
|
]);
|
|
return {
|
|
stats: projectStats,
|
|
name: project.name,
|
|
description: project.description,
|
|
mode: project.mode,
|
|
defaultStickiness: project.defaultStickiness,
|
|
health: project.health || 0,
|
|
favorite: favorite,
|
|
updatedAt: project.updatedAt,
|
|
environments,
|
|
features,
|
|
members,
|
|
version: 1,
|
|
};
|
|
}
|
|
|
|
async getProjectSettings(projectId: string): Promise<IProjectSettings> {
|
|
return this.store.getProjectSettings(projectId);
|
|
}
|
|
|
|
async setProjectSettings(
|
|
projectId: string,
|
|
defaultStickiness: string,
|
|
mode: ProjectMode,
|
|
): Promise<void> {
|
|
return this.store.setProjectSettings(
|
|
projectId,
|
|
defaultStickiness,
|
|
mode,
|
|
);
|
|
}
|
|
}
|