mirror of
https://github.com/snowball-tools/snowballtools-base.git
synced 2024-11-18 09:06:18 +00:00
Show domains for change to production and redeploy in deployments page (#56)
* Display URL for change to production dialog box * Refactor database method for domains to service class * Handle error in resolver instead of service class * Return entity from service class for add operation * Do not fetch branches if repo not available --------- Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
parent
7e522aa45d
commit
afd522654c
@ -1,4 +1,4 @@
|
||||
import { DataSource, DeepPartial, FindManyOptions, FindOneOptions } from 'typeorm';
|
||||
import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm';
|
||||
import path from 'path';
|
||||
import debug from 'debug';
|
||||
import assert from 'assert';
|
||||
@ -41,7 +41,7 @@ export class Database {
|
||||
return user;
|
||||
}
|
||||
|
||||
async createUser (data: DeepPartial<User>): Promise<User> {
|
||||
async addUser (data: DeepPartial<User>): Promise<User> {
|
||||
const userRepository = this.dataSource.getRepository(User);
|
||||
const user = await userRepository.save(data);
|
||||
|
||||
@ -161,7 +161,7 @@ export class Database {
|
||||
return domains;
|
||||
}
|
||||
|
||||
async createDeployement (data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||
async addDeployement (data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
const deployment = await deploymentRepository.save(data);
|
||||
|
||||
@ -216,11 +216,7 @@ export class Database {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
const updateResult = await projectMemberRepository.update({ id: Number(projectMemberId) }, data);
|
||||
|
||||
if (updateResult.affected) {
|
||||
return updateResult.affected > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> {
|
||||
@ -241,11 +237,7 @@ export class Database {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
const updateResult = await environmentVariableRepository.update({ id: Number(environmentVariableId) }, update);
|
||||
|
||||
if (updateResult.affected) {
|
||||
return updateResult.affected > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
||||
@ -302,11 +294,7 @@ export class Database {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
const updateResult = await deploymentRepository.update({ id: deploymentId }, updates);
|
||||
|
||||
if (updateResult.affected) {
|
||||
return updateResult.affected > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async addProject (userId: string, projectDetails: DeepPartial<Project>): Promise<Project> {
|
||||
@ -336,11 +324,7 @@ export class Database {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
const updateResult = await projectRepository.update({ id: projectId }, updates);
|
||||
|
||||
if (updateResult.affected) {
|
||||
return updateResult.affected > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteProjectById (projectId: string): Promise<boolean> {
|
||||
@ -371,70 +355,28 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async rollbackDeploymentById (projectId: string, deploymentId: string): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
|
||||
// TODO: Implement transactions
|
||||
const oldCurrentDeployment = await deploymentRepository.findOne({
|
||||
relations: {
|
||||
domain: true
|
||||
},
|
||||
where: {
|
||||
project: {
|
||||
id: projectId
|
||||
},
|
||||
isCurrent: true
|
||||
}
|
||||
});
|
||||
|
||||
const oldCurrentDeploymentUpdate = await deploymentRepository.update({ project: { id: projectId }, isCurrent: true }, { isCurrent: false, domain: null });
|
||||
|
||||
const newCurrentDeploymentUpdate = await deploymentRepository.update({ id: deploymentId }, { isCurrent: true, domain: oldCurrentDeployment?.domain });
|
||||
|
||||
if (oldCurrentDeploymentUpdate.affected && newCurrentDeploymentUpdate.affected) {
|
||||
return oldCurrentDeploymentUpdate.affected > 0 && newCurrentDeploymentUpdate.affected > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async addDomainByProjectId (projectId: string, domainDetails: { name: string }): Promise<Domain[]> {
|
||||
async addDomain (data: DeepPartial<Domain>): Promise<Domain> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
const newDomain = await domainRepository.save(data);
|
||||
|
||||
const currentProject = await projectRepository.findOneBy({
|
||||
id: projectId
|
||||
});
|
||||
|
||||
if (currentProject === null) {
|
||||
throw new Error(`Project with ${projectId} not found`);
|
||||
}
|
||||
|
||||
const primaryDomainDetails = {
|
||||
...domainDetails,
|
||||
branch: currentProject.prodBranch,
|
||||
project: currentProject
|
||||
};
|
||||
|
||||
const primaryDomain = domainRepository.create(primaryDomainDetails as DeepPartial<Domain>);
|
||||
const savedPrimaryDomain = await domainRepository.save(primaryDomain);
|
||||
|
||||
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 redirectedDomain = domainRepository.create(redirectedDomainDetails as DeepPartial<Domain>);
|
||||
const savedRedirectedDomain = await domainRepository.save(redirectedDomain);
|
||||
|
||||
return [savedPrimaryDomain, savedRedirectedDomain];
|
||||
return newDomain;
|
||||
}
|
||||
|
||||
async getDomainsByProjectId (projectId: string): Promise<Domain[]> {
|
||||
async getDomain (options: FindOneOptions<Domain>): Promise<Domain | null> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
const domain = await domainRepository.findOne(options);
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
async updateDomainById (domainId: string, updates: DeepPartial<Domain>): Promise<boolean> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
const updateResult = await domainRepository.update({ id: Number(domainId) }, updates);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
|
||||
const domains = await domainRepository.find({
|
||||
@ -444,68 +386,11 @@ export class Database {
|
||||
where: {
|
||||
project: {
|
||||
id: projectId
|
||||
}
|
||||
},
|
||||
...filter
|
||||
}
|
||||
});
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
|
||||
const domain = await domainRepository.findOne({
|
||||
where: {
|
||||
id: Number(domainId)
|
||||
}
|
||||
});
|
||||
|
||||
const newDomain: DeepPartial<Domain> = {
|
||||
...data
|
||||
};
|
||||
|
||||
if (domain === null) {
|
||||
throw new Error(`Error finding domain with id ${domainId}`);
|
||||
}
|
||||
|
||||
const domainsRedirectedFrom = await domainRepository.find({
|
||||
where: {
|
||||
project: {
|
||||
id: domain.projectId
|
||||
},
|
||||
redirectToId: domain.id
|
||||
}
|
||||
});
|
||||
|
||||
// If there are domains redirecting to current domain, only branch of current domain can be updated
|
||||
if (domainsRedirectedFrom.length > 0 && data.branch === domain.branch) {
|
||||
throw new Error('Remove all redirects to this domain before updating');
|
||||
}
|
||||
|
||||
if (data.redirectToId) {
|
||||
const redirectedDomain = await domainRepository.findOne({
|
||||
where: {
|
||||
id: Number(data.redirectToId)
|
||||
}
|
||||
});
|
||||
|
||||
if (redirectedDomain === null) {
|
||||
throw new Error('Could not find Domain to redirect to');
|
||||
}
|
||||
|
||||
if (redirectedDomain.redirectToId) {
|
||||
throw new Error('Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.');
|
||||
}
|
||||
|
||||
newDomain.redirectTo = redirectedDomain;
|
||||
}
|
||||
|
||||
const updateResult = await domainRepository.update({ id: Number(domainId) }, newDomain);
|
||||
|
||||
if (updateResult.affected) {
|
||||
return updateResult.affected > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,5 +55,5 @@ export class Domain {
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date;
|
||||
deletedAt!: Date | null;
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ export class Project {
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date;
|
||||
deletedAt!: Date | null;
|
||||
|
||||
@OneToMany(() => Deployment, (deployment) => deployment.project)
|
||||
deployments!: Deployment[];
|
||||
|
@ -47,5 +47,5 @@ export class ProjectMember {
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date;
|
||||
deletedAt!: Date | null;
|
||||
}
|
||||
|
@ -43,5 +43,5 @@ export class UserOrganization {
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date;
|
||||
deletedAt!: Date | null;
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import debug from 'debug';
|
||||
import assert from 'assert';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
||||
|
||||
import { OAuthApp } from '@octokit/oauth-app';
|
||||
|
||||
import { Service } from './service';
|
||||
import { Database } from './database';
|
||||
import { isUserOwner } from './utils';
|
||||
import { Permission } from './entity/ProjectMember';
|
||||
import { Domain } from './entity/Domain';
|
||||
import { Project } from './entity/Project';
|
||||
@ -51,8 +49,8 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
|
||||
return service.searchProjects(context.userId, searchText);
|
||||
},
|
||||
|
||||
domains: async (_:any, { projectId }: { projectId: string }) => {
|
||||
return service.getDomainsByProjectId(projectId);
|
||||
domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere<Domain> }) => {
|
||||
return service.getDomainsByProjectId(projectId, filter);
|
||||
}
|
||||
},
|
||||
|
||||
@ -60,20 +58,7 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
|
||||
Mutation: {
|
||||
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
|
||||
try {
|
||||
const member = await db.getProjectMemberById(projectMemberId);
|
||||
|
||||
if (member.member.id === context.userId) {
|
||||
throw new Error('Invalid operation: cannot remove self');
|
||||
}
|
||||
|
||||
const memberProject = member.project;
|
||||
assert(memberProject);
|
||||
|
||||
if (isUserOwner(String(context.userId), String(memberProject.owner.id))) {
|
||||
return await db.removeProjectMemberById(projectMemberId);
|
||||
} else {
|
||||
throw new Error('Invalid operation: not authorized');
|
||||
}
|
||||
return await service.removeProjectMember(context.userId, projectMemberId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@ -86,7 +71,12 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
|
||||
permissions: Permission[]
|
||||
}
|
||||
}) => {
|
||||
return service.updateProjectMember(projectMemberId, data);
|
||||
try {
|
||||
return await service.updateProjectMember(projectMemberId, data);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
addProjectMember: async (_: any, { projectId, data }: {
|
||||
@ -96,48 +86,97 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
|
||||
permissions: Permission[]
|
||||
}
|
||||
}) => {
|
||||
return service.addProjectMember(projectId, data);
|
||||
try {
|
||||
return Boolean(await service.addProjectMember(projectId, data));
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
addEnvironmentVariables: async (_: any, { projectId, data }: { projectId: string, data: { environments: string[], key: string, value: string}[] }) => {
|
||||
return service.addEnvironmentVariables(projectId, data);
|
||||
try {
|
||||
return Boolean(await service.addEnvironmentVariables(projectId, data));
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => {
|
||||
return service.updateEnvironmentVariable(environmentVariableId, data);
|
||||
try {
|
||||
return await service.updateEnvironmentVariable(environmentVariableId, data);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
|
||||
return service.removeEnvironmentVariable(environmentVariableId);
|
||||
try {
|
||||
return await service.removeEnvironmentVariable(environmentVariableId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }) => {
|
||||
return service.updateDeploymentToProd(deploymentId);
|
||||
try {
|
||||
return await service.updateDeploymentToProd(deploymentId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
addProject: async (_: any, { data }: { data: DeepPartial<Project> }, context: any) => {
|
||||
return service.addProject(context.userId, data);
|
||||
try {
|
||||
return Boolean(await service.addProject(context.userId, data));
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
updateProject: async (_: any, { projectId, projectDetails }: { projectId: string, projectDetails: DeepPartial<Project> }) => {
|
||||
return service.updateProject(projectId, projectDetails);
|
||||
try {
|
||||
return await service.updateProject(projectId, projectDetails);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
|
||||
return service.redeployToProd(context.userId, deploymentId);
|
||||
try {
|
||||
return await service.redeployToProd(context.userId, deploymentId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
deleteProject: async (_: any, { projectId }: { projectId: string }) => {
|
||||
return service.deleteProject(projectId);
|
||||
try {
|
||||
return await service.deleteProject(projectId);
|
||||
} catch (err) {
|
||||
log(err); return false;
|
||||
}
|
||||
},
|
||||
|
||||
deleteDomain: async (_: any, { domainId }: { domainId: string }) => {
|
||||
return service.deleteDomain(domainId);
|
||||
try {
|
||||
return await service.deleteDomain(domainId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => {
|
||||
try {
|
||||
return await db.rollbackDeploymentById(projectId, deploymentId);
|
||||
return await service.rollbackDeployment(projectId, deploymentId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@ -146,7 +185,7 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
|
||||
|
||||
addDomain: async (_: any, { projectId, domainDetails }: { projectId: string, domainDetails: { name: string } }) => {
|
||||
try {
|
||||
return await db.addDomainByProjectId(projectId, domainDetails);
|
||||
return Boolean(await service.addDomain(projectId, domainDetails));
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
@ -155,7 +194,7 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
|
||||
|
||||
updateDomain: async (_: any, { domainId, domainDetails }: { domainId: string, domainDetails: DeepPartial<Domain>}) => {
|
||||
try {
|
||||
return await db.updateDomainById(domainId, domainDetails);
|
||||
return await service.updateDomain(domainId, domainDetails);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
|
@ -167,6 +167,10 @@ input UpdateProjectMemberInput {
|
||||
permissions: [Permission]
|
||||
}
|
||||
|
||||
input FilterDomainsInput {
|
||||
branch: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
user: User!
|
||||
organizations: [Organization!]
|
||||
@ -177,7 +181,7 @@ type Query {
|
||||
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
||||
projectMembers(projectId: String!): [ProjectMember!]
|
||||
searchProjects(searchText: String!): [Project!]
|
||||
domains(projectId: String!): [Domain!]
|
||||
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import debug from 'debug';
|
||||
import assert from 'assert';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { lowercase, numbers } from 'nanoid-dictionary';
|
||||
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
||||
|
||||
import { Database } from './database';
|
||||
import { Deployment, Environment } from './entity/Deployment';
|
||||
@ -10,9 +11,7 @@ import { Organization } from './entity/Organization';
|
||||
import { Project } from './entity/Project';
|
||||
import { Permission, ProjectMember } from './entity/ProjectMember';
|
||||
import { User } from './entity/User';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
|
||||
const log = debug('snowball:service');
|
||||
import { isUserOwner } from './utils';
|
||||
|
||||
const nanoid = customAlphabet(lowercase + numbers, 8);
|
||||
|
||||
@ -66,193 +65,290 @@ export class Service {
|
||||
return dbProjects;
|
||||
}
|
||||
|
||||
async getDomainsByProjectId (projectId: string): Promise<Domain[]> {
|
||||
const dbDomains = await this.db.getDomainsByProjectId(projectId);
|
||||
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
||||
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
|
||||
return dbDomains;
|
||||
}
|
||||
|
||||
async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise<boolean> {
|
||||
try {
|
||||
return await this.db.updateProjectMemberById(projectMemberId, data);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
return this.db.updateProjectMemberById(projectMemberId, data);
|
||||
}
|
||||
|
||||
async addProjectMember (projectId: string,
|
||||
data: {
|
||||
email: string,
|
||||
permissions: Permission[]
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
// TODO: Send invitation
|
||||
let user = await this.db.getUser({
|
||||
where: {
|
||||
email: data.email
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = await this.db.createUser({
|
||||
email: data.email
|
||||
});
|
||||
}): Promise<ProjectMember> {
|
||||
// TODO: Send invitation
|
||||
let user = await this.db.getUser({
|
||||
where: {
|
||||
email: data.email
|
||||
}
|
||||
});
|
||||
|
||||
const newProjectMember = await this.db.addProjectMember({
|
||||
project: {
|
||||
id: projectId
|
||||
},
|
||||
permissions: data.permissions,
|
||||
isPending: true,
|
||||
member: {
|
||||
id: user.id
|
||||
}
|
||||
if (!user) {
|
||||
user = await this.db.addUser({
|
||||
email: data.email
|
||||
});
|
||||
}
|
||||
|
||||
return Boolean(newProjectMember);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
const newProjectMember = await this.db.addProjectMember({
|
||||
project: {
|
||||
id: projectId
|
||||
},
|
||||
permissions: data.permissions,
|
||||
isPending: true,
|
||||
member: {
|
||||
id: user.id
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
if (isUserOwner(String(userId), String(memberProject.owner.id))) {
|
||||
return this.db.removeProjectMemberById(projectMemberId);
|
||||
} else {
|
||||
throw new Error('Invalid operation: not authorized');
|
||||
}
|
||||
}
|
||||
|
||||
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<boolean> {
|
||||
try {
|
||||
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
|
||||
})
|
||||
});
|
||||
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
|
||||
})
|
||||
});
|
||||
}).flat();
|
||||
});
|
||||
}).flat();
|
||||
|
||||
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables);
|
||||
return savedEnvironmentVariables.length > 0;
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables);
|
||||
return savedEnvironmentVariables;
|
||||
}
|
||||
|
||||
async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
||||
try {
|
||||
return await this.db.updateEnvironmentVariable(environmentVariableId, data);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
return this.db.updateEnvironmentVariable(environmentVariableId, data);
|
||||
}
|
||||
|
||||
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
||||
try {
|
||||
return await this.db.deleteEnvironmentVariable(environmentVariableId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
||||
}
|
||||
|
||||
async updateDeploymentToProd (deploymentId: string): Promise<boolean> {
|
||||
try {
|
||||
return await this.db.updateDeploymentById(deploymentId, {
|
||||
environment: Environment.Production
|
||||
});
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
const updateResult = await this.db.updateDeploymentById(deploymentId, {
|
||||
environment: Environment.Production,
|
||||
domain: prodBranchDomains[0],
|
||||
isCurrent: true
|
||||
});
|
||||
|
||||
return updateResult;
|
||||
}
|
||||
|
||||
async addProject (userId: string, data: DeepPartial<Project>): Promise<boolean> {
|
||||
try {
|
||||
await this.db.addProject(userId, data);
|
||||
return true;
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
async addProject (userId: string, data: DeepPartial<Project>): Promise<Project> {
|
||||
return this.db.addProject(userId, data);
|
||||
}
|
||||
|
||||
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
|
||||
try {
|
||||
return await this.db.updateProjectById(projectId, data);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
return this.db.updateProjectById(projectId, data);
|
||||
}
|
||||
|
||||
async deleteProject (projectId: string): Promise<boolean> {
|
||||
try {
|
||||
return await this.db.deleteProjectById(projectId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
return this.db.deleteProjectById(projectId);
|
||||
}
|
||||
|
||||
async deleteDomain (domainId: string): Promise<boolean> {
|
||||
try {
|
||||
const domainsRedirectedFrom = await this.db.getDomains({
|
||||
where: {
|
||||
redirectToId: Number(domainId)
|
||||
}
|
||||
});
|
||||
|
||||
if (domainsRedirectedFrom.length > 0) {
|
||||
throw new Error('Cannot delete domain since it has redirects from other domains');
|
||||
const domainsRedirectedFrom = await this.db.getDomains({
|
||||
where: {
|
||||
redirectToId: Number(domainId)
|
||||
}
|
||||
});
|
||||
|
||||
return await this.db.deleteDomainById(domainId);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
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> {
|
||||
try {
|
||||
const deployment = await this.db.getDeployment({
|
||||
relations: {
|
||||
project: true,
|
||||
domain: true,
|
||||
createdBy: true
|
||||
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(), {
|
||||
id: Number(userId)
|
||||
});
|
||||
}
|
||||
|
||||
updatedDeployment.id = nanoid();
|
||||
updatedDeployment.url = `${updatedDeployment.id}-${updatedDeployment.project.subDomain}`;
|
||||
|
||||
const oldDeployment = await this.db.updateDeploymentById(deploymentId, { domain: null, isCurrent: false });
|
||||
const newDeployement = await this.db.addDeployement(updatedDeployment);
|
||||
|
||||
return oldDeployment && Boolean(newDeployement);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
if (!oldCurrentDeployment) {
|
||||
throw new Error('Current deployement doesnot exist');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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: {
|
||||
id: Number(domainId)
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
},
|
||||
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({
|
||||
where: {
|
||||
id: deploymentId
|
||||
id: Number(domainDetails.redirectToId)
|
||||
}
|
||||
});
|
||||
|
||||
if (deployment === null) {
|
||||
throw new Error('Deployment not found');
|
||||
if (redirectedDomain === null) {
|
||||
throw new Error('Could not find Domain to redirect to');
|
||||
}
|
||||
|
||||
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(), {
|
||||
id: Number(userId)
|
||||
});
|
||||
if (redirectedDomain.redirectToId) {
|
||||
throw new Error('Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.');
|
||||
}
|
||||
|
||||
updatedDeployment.id = nanoid();
|
||||
updatedDeployment.url = `${updatedDeployment.id}-${updatedDeployment.project.subDomain}`;
|
||||
|
||||
const oldDeployment = await this.db.updateDeploymentById(deploymentId, { domain: null, isCurrent: false });
|
||||
const newDeployement = await this.db.createDeployement(updatedDeployment);
|
||||
|
||||
return oldDeployment && Boolean(newDeployement);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
newDomain.redirectTo = redirectedDomain;
|
||||
}
|
||||
|
||||
const updateResult = await this.db.updateDomainById(domainId, newDomain);
|
||||
|
||||
return updateResult;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Project } from 'gql-client';
|
||||
import { Project, Domain } from 'gql-client';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
@ -22,6 +22,7 @@ const DeploymentsTabPanel = ({ project }: { project: Project }) => {
|
||||
|
||||
const [filterValue, setFilterValue] = useState(DEFAULT_FILTER_VALUE);
|
||||
const [deployments, setDeployments] = useState<DeploymentDetails[]>([]);
|
||||
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
|
||||
|
||||
const fetchDeployments = async () => {
|
||||
const { deployments } = await client.getDeployments(project.id);
|
||||
@ -35,8 +36,16 @@ const DeploymentsTabPanel = ({ project }: { project: Project }) => {
|
||||
setDeployments(updatedDeployments);
|
||||
};
|
||||
|
||||
const fetchProductionBranchDomains = useCallback(async () => {
|
||||
const { domains } = await client.getDomains(project.id, {
|
||||
branch: project.prodBranch,
|
||||
});
|
||||
setProdBranchDomains(domains);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDeployments();
|
||||
fetchProductionBranchDomains();
|
||||
}, []);
|
||||
|
||||
const currentDeployment = useMemo(() => {
|
||||
@ -93,6 +102,7 @@ const DeploymentsTabPanel = ({ project }: { project: Project }) => {
|
||||
currentDeployment={currentDeployment!}
|
||||
onUpdate={onUpdateDeploymenToProd}
|
||||
project={project}
|
||||
prodBranchDomains={prodBranchDomains}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
@ -31,6 +31,11 @@ const OverviewTabPanel = ({ project }: OverviewProps) => {
|
||||
const fetchRepoActivity = async () => {
|
||||
const [owner, repo] = project.repository.split('/');
|
||||
|
||||
if (!repo) {
|
||||
// Do not fetch branches if repo not available
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all branches in project repo
|
||||
const result = await octokit.rest.repos.listBranches({
|
||||
owner,
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
ChipProps,
|
||||
} from '@material-tailwind/react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Environment, Project } from 'gql-client';
|
||||
import { Environment, Project, Domain } from 'gql-client';
|
||||
|
||||
import { relativeTimeMs } from '../../../../utils/time';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
@ -24,6 +24,7 @@ interface DeployDetailsCardProps {
|
||||
currentDeployment: DeploymentDetails;
|
||||
onUpdate: () => Promise<void>;
|
||||
project: Project;
|
||||
prodBranchDomains: Domain[];
|
||||
}
|
||||
|
||||
const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = {
|
||||
@ -37,6 +38,7 @@ const DeploymentDetailsCard = ({
|
||||
currentDeployment,
|
||||
onUpdate,
|
||||
project,
|
||||
prodBranchDomains,
|
||||
}: DeployDetailsCardProps) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
@ -167,12 +169,14 @@ const DeploymentDetailsCard = ({
|
||||
<Typography variant="small">
|
||||
The new deployment will be associated with these domains:
|
||||
</Typography>
|
||||
<Typography variant="small" color="blue">
|
||||
^ saugatt.com
|
||||
</Typography>
|
||||
<Typography variant="small" color="blue">
|
||||
^ www.saugatt.com
|
||||
</Typography>
|
||||
{prodBranchDomains.length > 0 &&
|
||||
prodBranchDomains.map((value) => {
|
||||
return (
|
||||
<Typography variant="small" color="blue" key={value.id}>
|
||||
^ {value.name}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
<ConfirmDialog
|
||||
@ -195,12 +199,11 @@ const DeploymentDetailsCard = ({
|
||||
<Typography variant="small">
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
<Typography variant="small" color="blue">
|
||||
^ saugatt.com
|
||||
</Typography>
|
||||
<Typography variant="small" color="blue">
|
||||
^ www.saugatt.com
|
||||
</Typography>
|
||||
{deployment.domain?.name && (
|
||||
<Typography variant="small" color="blue">
|
||||
{deployment.domain?.name}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
<ConfirmDialog
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
||||
|
||||
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getDomains, getProjectsInOrganization } from './queries';
|
||||
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGitHubResponse, UnauthenticateGitHubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse, UpdateProjectMemberInput, RemoveProjectMemberResponse, UpdateProjectMemberResponse, DeleteDomainResponse, AddProjectMemberInput, AddProjectMemberResponse, AddProjectInput, AddProjectResponse } from './types';
|
||||
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGitHubResponse, UnauthenticateGitHubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse, UpdateProjectMemberInput, RemoveProjectMemberResponse, UpdateProjectMemberResponse, DeleteDomainResponse, AddProjectMemberInput, AddProjectMemberResponse, AddProjectInput, AddProjectResponse, FilterDomainInput } from './types';
|
||||
import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember, deleteDomain, addProjectMember, addProject } from './mutations';
|
||||
|
||||
export interface GraphQLConfig {
|
||||
@ -286,11 +286,12 @@ export class GQLClient {
|
||||
return data;
|
||||
}
|
||||
|
||||
async getDomains (projectId: string): Promise<GetDomainsResponse> {
|
||||
async getDomains (projectId: string, filter?: FilterDomainInput): Promise<GetDomainsResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: getDomains,
|
||||
variables: {
|
||||
projectId
|
||||
projectId,
|
||||
filter
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -199,8 +199,8 @@ query ($searchText: String!) {
|
||||
`;
|
||||
|
||||
export const getDomains = gql`
|
||||
query ($projectId: String!) {
|
||||
domains(projectId: $projectId) {
|
||||
query ($projectId: String!, $filter: FilterDomainsInput) {
|
||||
domains(projectId: $projectId, filter: $filter) {
|
||||
branch
|
||||
createdAt
|
||||
redirectTo {
|
||||
|
@ -272,6 +272,10 @@ export type AddDomainInput = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type FilterDomainInput = {
|
||||
branch: string
|
||||
}
|
||||
|
||||
export type AddDomainResponse = {
|
||||
addDomain: true
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user