2024-02-06 08:48:06 +00:00
|
|
|
import assert from 'assert';
|
|
|
|
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
2024-02-05 10:51:55 +00:00
|
|
|
|
2024-02-08 09:29:19 +00:00
|
|
|
import { OAuthApp } from '@octokit/oauth-app';
|
|
|
|
|
2024-02-02 09:32:12 +00:00
|
|
|
import { Database } from './database';
|
2024-02-05 10:51:55 +00:00
|
|
|
import { Deployment, Environment } from './entity/Deployment';
|
2024-02-02 09:32:12 +00:00
|
|
|
import { Domain } from './entity/Domain';
|
|
|
|
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
|
|
|
import { Organization } from './entity/Organization';
|
|
|
|
import { Project } from './entity/Project';
|
2024-02-05 10:51:55 +00:00
|
|
|
import { Permission, ProjectMember } from './entity/ProjectMember';
|
2024-02-02 09:32:12 +00:00
|
|
|
import { User } from './entity/User';
|
|
|
|
export class Service {
|
|
|
|
private db: Database;
|
2024-02-08 09:29:19 +00:00
|
|
|
private app: OAuthApp;
|
2024-02-02 09:32:12 +00:00
|
|
|
|
2024-02-08 09:29:19 +00:00
|
|
|
constructor (db: Database, app: OAuthApp) {
|
2024-02-02 09:32:12 +00:00
|
|
|
this.db = db;
|
2024-02-08 09:29:19 +00:00
|
|
|
this.app = app;
|
2024-02-02 09:32:12 +00:00
|
|
|
}
|
|
|
|
|
2024-02-06 13:41:53 +00:00
|
|
|
async getUser (userId: string): Promise<User | null> {
|
2024-02-05 10:51:55 +00:00
|
|
|
return this.db.getUser({
|
|
|
|
where: {
|
|
|
|
id: userId
|
|
|
|
}
|
|
|
|
});
|
2024-02-02 09:32:12 +00:00
|
|
|
}
|
|
|
|
|
2024-02-06 13:41:53 +00:00
|
|
|
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
2024-02-02 09:32:12 +00:00
|
|
|
const dbOrganizations = await this.db.getOrganizationsByUserId(userId);
|
|
|
|
return dbOrganizations;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getProjectById (projectId: string): Promise<Project | null> {
|
|
|
|
const dbProject = await this.db.getProjectById(projectId);
|
|
|
|
return dbProject;
|
|
|
|
}
|
|
|
|
|
2024-02-07 13:11:54 +00:00
|
|
|
async getProjectsInOrganization (userId:string, organizationSlug: string): Promise<Project[]> {
|
|
|
|
const dbProjects = await this.db.getProjectsInOrganization(userId, organizationSlug);
|
2024-02-02 09:32:12 +00:00
|
|
|
return dbProjects;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getDeployementsByProjectId (projectId: string): Promise<Deployment[]> {
|
|
|
|
const dbDeployments = await this.db.getDeploymentsByProjectId(projectId);
|
|
|
|
return dbDeployments;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getEnvironmentVariablesByProjectId (projectId: string): Promise<EnvironmentVariable[]> {
|
|
|
|
const dbEnvironmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId);
|
|
|
|
return dbEnvironmentVariables;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
|
|
|
const dbProjectMembers = await this.db.getProjectMembersByProjectId(projectId);
|
|
|
|
return dbProjectMembers;
|
|
|
|
}
|
|
|
|
|
2024-02-06 13:41:53 +00:00
|
|
|
async searchProjects (userId: string, searchText: string): Promise<Project[]> {
|
|
|
|
const dbProjects = await this.db.getProjectsBySearchText(userId, searchText);
|
2024-02-02 09:32:12 +00:00
|
|
|
return dbProjects;
|
|
|
|
}
|
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
|
|
|
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
|
2024-02-02 09:32:12 +00:00
|
|
|
return dbDomains;
|
|
|
|
}
|
2024-02-05 10:51:55 +00:00
|
|
|
|
|
|
|
async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise<boolean> {
|
2024-02-06 08:48:06 +00:00
|
|
|
return this.db.updateProjectMemberById(projectMemberId, data);
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async addProjectMember (projectId: string,
|
|
|
|
data: {
|
|
|
|
email: string,
|
|
|
|
permissions: Permission[]
|
2024-02-06 08:48:06 +00:00
|
|
|
}): Promise<ProjectMember> {
|
|
|
|
// TODO: Send invitation
|
|
|
|
let user = await this.db.getUser({
|
|
|
|
where: {
|
|
|
|
email: data.email
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
user = await this.db.addUser({
|
|
|
|
email: data.email
|
2024-02-05 10:51:55 +00:00
|
|
|
});
|
2024-02-06 08:48:06 +00:00
|
|
|
}
|
2024-02-05 10:51:55 +00:00
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
const newProjectMember = await this.db.addProjectMember({
|
|
|
|
project: {
|
|
|
|
id: projectId
|
|
|
|
},
|
|
|
|
permissions: data.permissions,
|
|
|
|
isPending: true,
|
|
|
|
member: {
|
|
|
|
id: user.id
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
2024-02-06 08:48:06 +00:00
|
|
|
});
|
2024-02-05 10:51:55 +00:00
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
return newProjectMember;
|
|
|
|
}
|
|
|
|
|
|
|
|
async removeProjectMember (userId: string, projectMemberId: string): Promise<boolean> {
|
|
|
|
const member = await this.db.getProjectMemberById(projectMemberId);
|
|
|
|
|
|
|
|
if (String(member.member.id) === userId) {
|
|
|
|
throw new Error('Invalid operation: cannot remove self');
|
|
|
|
}
|
|
|
|
|
|
|
|
const memberProject = member.project;
|
|
|
|
assert(memberProject);
|
2024-02-05 10:51:55 +00:00
|
|
|
|
2024-02-06 10:48:29 +00:00
|
|
|
if (String(userId) === String(memberProject.owner.id)) {
|
2024-02-06 08:48:06 +00:00
|
|
|
return this.db.removeProjectMemberById(projectMemberId);
|
|
|
|
} else {
|
|
|
|
throw new Error('Invalid operation: not authorized');
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<EnvironmentVariable[]> {
|
|
|
|
const formattedEnvironmentVariables = data.map((environmentVariable) => {
|
|
|
|
return environmentVariable.environments.map((environment) => {
|
|
|
|
return ({
|
|
|
|
key: environmentVariable.key,
|
|
|
|
value: environmentVariable.value,
|
|
|
|
environment: environment as Environment,
|
|
|
|
project: Object.assign(new Project(), {
|
|
|
|
id: projectId
|
|
|
|
})
|
2024-02-05 10:51:55 +00:00
|
|
|
});
|
2024-02-06 08:48:06 +00:00
|
|
|
});
|
|
|
|
}).flat();
|
2024-02-05 10:51:55 +00:00
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables);
|
|
|
|
return savedEnvironmentVariables;
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
2024-02-06 08:48:06 +00:00
|
|
|
return this.db.updateEnvironmentVariable(environmentVariableId, data);
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
2024-02-06 08:48:06 +00:00
|
|
|
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
|
2024-02-08 09:29:19 +00:00
|
|
|
async updateDeploymentToProd (userId: string, deploymentId: string): Promise<Deployment> {
|
2024-02-06 08:48:06 +00:00
|
|
|
const deployment = await this.db.getDeployment({ where: { id: deploymentId }, relations: { project: true } });
|
|
|
|
|
|
|
|
if (!deployment) {
|
|
|
|
throw new Error('Deployment does not exist');
|
|
|
|
}
|
|
|
|
|
|
|
|
const prodBranchDomains = await this.db.getDomainsByProjectId(deployment.project.id, { branch: deployment.project.prodBranch });
|
|
|
|
|
|
|
|
const oldDeployment = await this.db.getDeployment({
|
|
|
|
where: {
|
|
|
|
domain: {
|
|
|
|
id: prodBranchDomains[0].id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (oldDeployment) {
|
|
|
|
await this.db.updateDeploymentById(oldDeployment.id, {
|
|
|
|
domain: null,
|
|
|
|
isCurrent: false
|
2024-02-05 10:51:55 +00:00
|
|
|
});
|
|
|
|
}
|
2024-02-06 08:48:06 +00:00
|
|
|
|
2024-02-08 09:29:19 +00:00
|
|
|
const { createdAt, updatedAt, ...updatedDeployment } = deployment;
|
|
|
|
|
|
|
|
updatedDeployment.isCurrent = true;
|
|
|
|
updatedDeployment.environment = Environment.Production;
|
|
|
|
updatedDeployment.domain = prodBranchDomains[0];
|
|
|
|
updatedDeployment.createdBy = Object.assign(new User(), {
|
|
|
|
id: userId
|
2024-02-06 08:48:06 +00:00
|
|
|
});
|
|
|
|
|
2024-02-08 09:29:19 +00:00
|
|
|
const newDeployement = await this.db.addDeployement(updatedDeployment);
|
|
|
|
|
|
|
|
return newDeployement;
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
|
2024-02-07 13:11:54 +00:00
|
|
|
async addProject (userId: string, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
|
|
|
|
const organization = await this.db.getOrganization({
|
|
|
|
where: {
|
|
|
|
slug: organizationSlug
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (!organization) {
|
|
|
|
throw new Error('Organization does not exist');
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.db.addProject(userId, organization.id, data);
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
|
2024-02-06 08:48:06 +00:00
|
|
|
return this.db.updateProjectById(projectId, data);
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async deleteProject (projectId: string): Promise<boolean> {
|
2024-02-06 08:48:06 +00:00
|
|
|
return this.db.deleteProjectById(projectId);
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async deleteDomain (domainId: string): Promise<boolean> {
|
2024-02-06 08:48:06 +00:00
|
|
|
const domainsRedirectedFrom = await this.db.getDomains({
|
|
|
|
where: {
|
2024-02-06 13:41:53 +00:00
|
|
|
redirectToId: domainId
|
2024-02-06 08:48:06 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (domainsRedirectedFrom.length > 0) {
|
|
|
|
throw new Error('Cannot delete domain since it has redirects from other domains');
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.db.deleteDomainById(domainId);
|
|
|
|
}
|
|
|
|
|
|
|
|
async redeployToProd (userId: string, deploymentId: string): Promise<Deployment| boolean> {
|
|
|
|
const deployment = await this.db.getDeployment({
|
|
|
|
relations: {
|
|
|
|
project: true,
|
|
|
|
domain: true,
|
|
|
|
createdBy: true
|
|
|
|
},
|
|
|
|
where: {
|
|
|
|
id: deploymentId
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (deployment === null) {
|
|
|
|
throw new Error('Deployment not found');
|
|
|
|
}
|
|
|
|
|
|
|
|
const { createdAt, updatedAt, ...updatedDeployment } = deployment;
|
|
|
|
|
|
|
|
if (updatedDeployment.environment === Environment.Production) {
|
|
|
|
// TODO: Put isCurrent field in project
|
|
|
|
updatedDeployment.isCurrent = true;
|
|
|
|
updatedDeployment.createdBy = Object.assign(new User(), {
|
2024-02-06 13:41:53 +00:00
|
|
|
id: userId
|
2024-02-05 10:51:55 +00:00
|
|
|
});
|
2024-02-06 08:48:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const oldDeployment = await this.db.updateDeploymentById(deploymentId, { domain: null, isCurrent: false });
|
|
|
|
const newDeployement = await this.db.addDeployement(updatedDeployment);
|
|
|
|
|
|
|
|
return oldDeployment && Boolean(newDeployement);
|
|
|
|
}
|
2024-02-05 10:51:55 +00:00
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> {
|
|
|
|
// TODO: Implement transactions
|
|
|
|
const oldCurrentDeployment = await this.db.getDeployment({
|
|
|
|
relations: {
|
|
|
|
domain: true
|
|
|
|
},
|
|
|
|
where: {
|
|
|
|
project: {
|
|
|
|
id: projectId
|
|
|
|
},
|
|
|
|
isCurrent: true
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
2024-02-06 08:48:06 +00:00
|
|
|
});
|
2024-02-05 10:51:55 +00:00
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
if (!oldCurrentDeployment) {
|
|
|
|
throw new Error('Current deployement doesnot exist');
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
2024-02-06 08:48:06 +00:00
|
|
|
|
|
|
|
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null });
|
|
|
|
|
|
|
|
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(deploymentId, { isCurrent: true, domain: oldCurrentDeployment?.domain });
|
|
|
|
|
|
|
|
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
async addDomain (projectId: string, domainDetails: { name: string }): Promise<{
|
|
|
|
primaryDomain: Domain,
|
|
|
|
redirectedDomain: Domain
|
|
|
|
}> {
|
|
|
|
const currentProject = await this.db.getProjectById(projectId);
|
|
|
|
|
|
|
|
if (currentProject === null) {
|
|
|
|
throw new Error(`Project with ${projectId} not found`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const primaryDomainDetails = {
|
|
|
|
...domainDetails,
|
|
|
|
branch: currentProject.prodBranch,
|
|
|
|
project: currentProject
|
|
|
|
};
|
|
|
|
|
|
|
|
const savedPrimaryDomain = await this.db.addDomain(primaryDomainDetails);
|
|
|
|
|
|
|
|
const domainArr = domainDetails.name.split('www.');
|
|
|
|
|
|
|
|
const redirectedDomainDetails = {
|
|
|
|
name: domainArr.length > 1 ? domainArr[1] : `www.${domainArr[0]}`,
|
|
|
|
branch: currentProject.prodBranch,
|
|
|
|
project: currentProject,
|
|
|
|
redirectTo: savedPrimaryDomain
|
|
|
|
};
|
|
|
|
|
|
|
|
const savedRedirectedDomain = await this.db.addDomain(redirectedDomainDetails);
|
|
|
|
|
|
|
|
return { primaryDomain: savedPrimaryDomain, redirectedDomain: savedRedirectedDomain };
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateDomain (domainId: string, domainDetails: DeepPartial<Domain>): Promise<boolean> {
|
|
|
|
const domain = await this.db.getDomain({
|
|
|
|
where: {
|
2024-02-06 13:41:53 +00:00
|
|
|
id: domainId
|
2024-02-06 08:48:06 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (domain === null) {
|
|
|
|
throw new Error(`Error finding domain with id ${domainId}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const newDomain = {
|
|
|
|
...domainDetails
|
|
|
|
};
|
|
|
|
|
|
|
|
const domainsRedirectedFrom = await this.db.getDomains({
|
|
|
|
where: {
|
|
|
|
project: {
|
|
|
|
id: domain.projectId
|
2024-02-05 10:51:55 +00:00
|
|
|
},
|
2024-02-06 08:48:06 +00:00
|
|
|
redirectToId: domain.id
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// If there are domains redirecting to current domain, only branch of current domain can be updated
|
|
|
|
if (domainsRedirectedFrom.length > 0 && domainDetails.branch === domain.branch) {
|
|
|
|
throw new Error('Remove all redirects to this domain before updating');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (domainDetails.redirectToId) {
|
|
|
|
const redirectedDomain = await this.db.getDomain({
|
2024-02-05 10:51:55 +00:00
|
|
|
where: {
|
2024-02-06 13:41:53 +00:00
|
|
|
id: domainDetails.redirectToId
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
if (redirectedDomain === null) {
|
|
|
|
throw new Error('Could not find Domain to redirect to');
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
if (redirectedDomain.redirectToId) {
|
|
|
|
throw new Error('Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.');
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
newDomain.redirectTo = redirectedDomain;
|
|
|
|
}
|
2024-02-05 10:51:55 +00:00
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
const updateResult = await this.db.updateDomainById(domainId, newDomain);
|
2024-02-05 10:51:55 +00:00
|
|
|
|
2024-02-06 08:48:06 +00:00
|
|
|
return updateResult;
|
2024-02-05 10:51:55 +00:00
|
|
|
}
|
2024-02-08 09:29:19 +00:00
|
|
|
|
|
|
|
async authenticateGitHub (code:string, userId: string): Promise<{token: string}> {
|
|
|
|
const { authentication: { token } } = await this.app.createToken({
|
|
|
|
code
|
|
|
|
});
|
|
|
|
|
|
|
|
await this.db.updateUser(userId, { gitHubToken: token });
|
|
|
|
|
|
|
|
return { token };
|
|
|
|
}
|
|
|
|
|
|
|
|
async unauthenticateGitHub (userId: string, data: DeepPartial<User>): Promise<boolean> {
|
|
|
|
return this.db.updateUser(userId, data);
|
|
|
|
}
|
2024-02-02 09:32:12 +00:00
|
|
|
}
|