From afd522654cad39969a7fd30281332d594745f924 Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Tue, 6 Feb 2024 14:18:06 +0530 Subject: [PATCH] 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 --- packages/backend/src/database.ts | 169 ++------ packages/backend/src/entity/Domain.ts | 2 +- packages/backend/src/entity/Project.ts | 2 +- packages/backend/src/entity/ProjectMember.ts | 2 +- .../backend/src/entity/UserOrganization.ts | 2 +- packages/backend/src/resolvers.ts | 105 +++-- packages/backend/src/schema.gql | 6 +- packages/backend/src/service.ts | 368 +++++++++++------- .../projects/project/DeploymentsTabPanel.tsx | 12 +- .../projects/project/OverviewTabPanel.tsx | 5 + .../deployments/DeploymentDetailsCard.tsx | 29 +- packages/gql-client/src/client.ts | 7 +- packages/gql-client/src/queries.ts | 4 +- packages/gql-client/src/types.ts | 4 + 14 files changed, 382 insertions(+), 335 deletions(-) diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 4c1c1a31..9d5cd3ef 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -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): Promise { + async addUser (data: DeepPartial): Promise { 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): Promise { + async addDeployement (data: DeepPartial): Promise { 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): Promise { @@ -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 { @@ -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): Promise { @@ -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 { @@ -371,70 +355,28 @@ export class Database { } } - async rollbackDeploymentById (projectId: string, deploymentId: string): Promise { - 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 { + async addDomain (data: DeepPartial): Promise { 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); - 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); - const savedRedirectedDomain = await domainRepository.save(redirectedDomain); - - return [savedPrimaryDomain, savedRedirectedDomain]; + return newDomain; } - async getDomainsByProjectId (projectId: string): Promise { + async getDomain (options: FindOneOptions): Promise { + const domainRepository = this.dataSource.getRepository(Domain); + const domain = await domainRepository.findOne(options); + + return domain; + } + + async updateDomainById (domainId: string, updates: DeepPartial): Promise { + 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): Promise { 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): Promise { - const domainRepository = this.dataSource.getRepository(Domain); - - const domain = await domainRepository.findOne({ - where: { - id: Number(domainId) - } - }); - - const newDomain: DeepPartial = { - ...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; - } - } } diff --git a/packages/backend/src/entity/Domain.ts b/packages/backend/src/entity/Domain.ts index b1f44515..f4a02026 100644 --- a/packages/backend/src/entity/Domain.ts +++ b/packages/backend/src/entity/Domain.ts @@ -55,5 +55,5 @@ export class Domain { updatedAt!: Date; @DeleteDateColumn() - deletedAt?: Date; + deletedAt!: Date | null; } diff --git a/packages/backend/src/entity/Project.ts b/packages/backend/src/entity/Project.ts index 187707b0..74e19df0 100644 --- a/packages/backend/src/entity/Project.ts +++ b/packages/backend/src/entity/Project.ts @@ -67,7 +67,7 @@ export class Project { updatedAt!: Date; @DeleteDateColumn() - deletedAt?: Date; + deletedAt!: Date | null; @OneToMany(() => Deployment, (deployment) => deployment.project) deployments!: Deployment[]; diff --git a/packages/backend/src/entity/ProjectMember.ts b/packages/backend/src/entity/ProjectMember.ts index b93eed70..5a361752 100644 --- a/packages/backend/src/entity/ProjectMember.ts +++ b/packages/backend/src/entity/ProjectMember.ts @@ -47,5 +47,5 @@ export class ProjectMember { updatedAt!: Date; @DeleteDateColumn() - deletedAt?: Date; + deletedAt!: Date | null; } diff --git a/packages/backend/src/entity/UserOrganization.ts b/packages/backend/src/entity/UserOrganization.ts index ceeac4fb..4c9195c1 100644 --- a/packages/backend/src/entity/UserOrganization.ts +++ b/packages/backend/src/entity/UserOrganization.ts @@ -43,5 +43,5 @@ export class UserOrganization { updatedAt!: Date; @DeleteDateColumn() - deletedAt?: Date; + deletedAt!: Date | null; } diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index b58855ff..99a93b09 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -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 }) => { + 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}) => { - 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 }, 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 }) => { - 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}) => { try { - return await db.updateDomainById(domainId, domainDetails); + return await service.updateDomain(domainId, domainDetails); } catch (err) { log(err); return false; diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index 3bfef2f7..5315c88b 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -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 { diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index ed2272c4..dfe68d41 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -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 { - const dbDomains = await this.db.getDomainsByProjectId(projectId); + async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere): Promise { + const dbDomains = await this.db.getDomainsByProjectId(projectId, filter); return dbDomains; } async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise { - 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 { - 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 { + // 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 { + 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 { - 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 { + 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): Promise { - 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 { - try { - return await this.db.deleteEnvironmentVariable(environmentVariableId); - } catch (err) { - log(err); - return false; - } + return this.db.deleteEnvironmentVariable(environmentVariableId); } async updateDeploymentToProd (deploymentId: string): Promise { - 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): Promise { - try { - await this.db.addProject(userId, data); - return true; - } catch (err) { - log(err); - return false; - } + async addProject (userId: string, data: DeepPartial): Promise { + return this.db.addProject(userId, data); } async updateProject (projectId: string, data: DeepPartial): Promise { - 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 { - try { - return await this.db.deleteProjectById(projectId); - } catch (err) { - log(err); - return false; - } + return this.db.deleteProjectById(projectId); } async deleteDomain (domainId: string): Promise { - 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 { - 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 { + // 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): Promise { + 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; } } diff --git a/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx b/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx index aba7bbec..9a346985 100644 --- a/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx +++ b/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx @@ -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([]); + const [prodBranchDomains, setProdBranchDomains] = useState([]); 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} /> ); }) diff --git a/packages/frontend/src/components/projects/project/OverviewTabPanel.tsx b/packages/frontend/src/components/projects/project/OverviewTabPanel.tsx index 7d1999ca..db7b6447 100644 --- a/packages/frontend/src/components/projects/project/OverviewTabPanel.tsx +++ b/packages/frontend/src/components/projects/project/OverviewTabPanel.tsx @@ -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, diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx index c030c66e..f6360943 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx @@ -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; 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 = ({ The new deployment will be associated with these domains: - - ^ saugatt.com - - - ^ www.saugatt.com - + {prodBranchDomains.length > 0 && + prodBranchDomains.map((value) => { + return ( + + ^ {value.name} + + ); + })} These domains will point to your new deployment: - - ^ saugatt.com - - - ^ www.saugatt.com - + {deployment.domain?.name && ( + + {deployment.domain?.name} + + )} { + async getDomains (projectId: string, filter?: FilterDomainInput): Promise { const { data } = await this.client.query({ query: getDomains, variables: { - projectId + projectId, + filter } }); diff --git a/packages/gql-client/src/queries.ts b/packages/gql-client/src/queries.ts index 150a68eb..a39576f9 100644 --- a/packages/gql-client/src/queries.ts +++ b/packages/gql-client/src/queries.ts @@ -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 { diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index 74d01d66..15ebe345 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -272,6 +272,10 @@ export type AddDomainInput = { name: string } +export type FilterDomainInput = { + branch: string +} + export type AddDomainResponse = { addDomain: true }