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:
Nabarun Gogoi 2024-02-06 14:18:06 +05:30 committed by GitHub
parent 7e522aa45d
commit afd522654c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 382 additions and 335 deletions

View File

@ -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;
}
}
}

View File

@ -55,5 +55,5 @@ export class Domain {
updatedAt!: Date;
@DeleteDateColumn()
deletedAt?: Date;
deletedAt!: Date | null;
}

View File

@ -67,7 +67,7 @@ export class Project {
updatedAt!: Date;
@DeleteDateColumn()
deletedAt?: Date;
deletedAt!: Date | null;
@OneToMany(() => Deployment, (deployment) => deployment.project)
deployments!: Deployment[];

View File

@ -47,5 +47,5 @@ export class ProjectMember {
updatedAt!: Date;
@DeleteDateColumn()
deletedAt?: Date;
deletedAt!: Date | null;
}

View File

@ -43,5 +43,5 @@ export class UserOrganization {
updatedAt!: Date;
@DeleteDateColumn()
deletedAt?: Date;
deletedAt!: Date | null;
}

View File

@ -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;

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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}
/>
);
})

View File

@ -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,

View File

@ -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

View File

@ -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
}
});

View File

@ -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 {

View File

@ -272,6 +272,10 @@ export type AddDomainInput = {
name: string
}
export type FilterDomainInput = {
branch: string
}
export type AddDomainResponse = {
addDomain: true
}