diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index cc8cdec..702842f 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -378,24 +378,22 @@ export class Database { const primaryDomainDetails = { ...domainDetails, - isRedirected: false, 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]}`, - isRedirected: true, branch: currentProject.prodBranch, - project: currentProject + project: currentProject, + redirectTo: savedPrimaryDomain }; - const savedPrimaryDomain = await domainRepository.save(primaryDomain); - const redirectedDomain = domainRepository.create(redirectedDomainDetails as DeepPartial); const savedRedirectedDomain = await domainRepository.save(redirectedDomain); @@ -406,6 +404,9 @@ export class Database { const domainRepository = this.dataSource.getRepository(Domain); const domains = await domainRepository.find({ + relations: { + redirectTo: true + }, where: { project: { id: projectId @@ -416,10 +417,56 @@ export class Database { return domains; } - async updateDomainById (domainId: string, updates: DeepPartial): Promise { + async updateDomainById (domainId: string, data: DeepPartial): Promise { const domainRepository = this.dataSource.getRepository(Domain); - const updateResult = await domainRepository.update({ id: Number(domainId) }, updates); + 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; diff --git a/packages/backend/src/entity/Domain.ts b/packages/backend/src/entity/Domain.ts index e72854a..5f6fde2 100644 --- a/packages/backend/src/entity/Domain.ts +++ b/packages/backend/src/entity/Domain.ts @@ -20,6 +20,9 @@ export class Domain { @PrimaryGeneratedColumn() id!: number; + @Column('varchar') + projectId!: string; + @ManyToOne(() => Project, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'projectId' }) project!: Project; @@ -30,8 +33,13 @@ export class Domain { @Column('varchar', { length: 255 }) name!: string; - @Column('boolean', { default: false }) - isRedirected!: boolean; + @Column('int', { nullable: true }) + redirectToId!: number; + + @ManyToOne(() => Domain) + @JoinColumn({ name: 'redirectToId' }) + // eslint-disable-next-line no-use-before-define + redirectTo!: Domain | null; @Column({ enum: Status, diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index 143975e..45434a3 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -1,5 +1,6 @@ import debug from 'debug'; import assert from 'assert'; +import { DeepPartial } from 'typeorm'; import { OAuthApp } from '@octokit/oauth-app'; @@ -7,6 +8,7 @@ import { Database } from './database'; import { deploymentToGqlType, projectMemberToGqlType, projectToGqlType, environmentVariableToGqlType, isUserOwner } from './utils'; import { Environment } from './entity/Deployment'; import { Permission } from './entity/ProjectMember'; +import { Domain } from './entity/Domain'; const log = debug('snowball:database'); @@ -233,7 +235,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise } }, - updateDomain: async (_: any, { domainId, domainDetails }: { domainId: string, domainDetails: {name?: string, isRedirected?: boolean, branch?: string }}) => { + updateDomain: async (_: any, { domainId, domainDetails }: { domainId: string, domainDetails: DeepPartial}) => { try { await db.updateDomainById(domainId, domainDetails); return true; diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index a7d8ac6..9a78803 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -98,7 +98,7 @@ type Domain { id: String! branch: String! name: String! - isRedirected: Boolean! + redirectTo: Domain status: DomainStatus! createdAt: String! updatedAt: String! @@ -163,8 +163,8 @@ input AddDomainInput { input UpdateDomainInput { name: String - isRedirected: Boolean branch: String + redirectToId: String } input UpdateEnvironmentVariableInput { diff --git a/packages/backend/test/fixtures/domains.json b/packages/backend/test/fixtures/primary-domains.json similarity index 54% rename from packages/backend/test/fixtures/domains.json rename to packages/backend/test/fixtures/primary-domains.json index 63fd462..4c31d4b 100644 --- a/packages/backend/test/fixtures/domains.json +++ b/packages/backend/test/fixtures/primary-domains.json @@ -3,63 +3,36 @@ "projectIndex": 0, "name": "randomurl.snowballtools.xyz", "status": "Live", - "isRedirected": false, "branch": "main" }, { "projectIndex": 0, "name": "saugatt.com", "status": "Pending", - "isRedirected": false, - "branch": "test" - }, - { - "projectIndex": 0, - "name": "www.saugatt.com", - "status": "Pending", - "isRedirected": true, "branch": "test" }, { "projectIndex": 1, "name": "randomurl.snowballtools.xyz", "status": "Live", - "isRedirected": false, "branch": "main" }, { "projectIndex": 1, "name": "saugatt.com", "status": "Pending", - "isRedirected": false, - "branch": "test" - }, - { - "projectIndex": 1, - "name": "www.saugatt.com", - "status": "Pending", - "isRedirected": true, "branch": "test" }, { "projectIndex": 2, "name": "randomurl.snowballtools.xyz", "status": "Live", - "isRedirected": false, "branch": "main" }, { "projectIndex": 2, "name": "saugatt.com", "status": "Pending", - "isRedirected": false, - "branch": "test" - }, - { - "projectIndex": 2, - "name": "www.saugatt.com", - "status": "Pending", - "isRedirected": true, "branch": "test" } ] diff --git a/packages/backend/test/fixtures/redirected-domains.json b/packages/backend/test/fixtures/redirected-domains.json new file mode 100644 index 0000000..d526bde --- /dev/null +++ b/packages/backend/test/fixtures/redirected-domains.json @@ -0,0 +1,23 @@ +[ + { + "projectIndex": 0, + "name": "www.saugatt.com", + "status": "Pending", + "redirectToIndex": 1, + "branch": "test" + }, + { + "projectIndex": 1, + "name": "www.saugatt.com", + "status": "Pending", + "redirectToIndex": 3, + "branch": "test" + }, + { + "projectIndex": 2, + "name": "www.saugatt.com", + "status": "Pending", + "redirectToIndex": 5, + "branch": "test" + } +] diff --git a/packages/backend/test/initialize-db.ts b/packages/backend/test/initialize-db.ts index 69f61fb..f28afaa 100644 --- a/packages/backend/test/initialize-db.ts +++ b/packages/backend/test/initialize-db.ts @@ -22,9 +22,10 @@ const PROJECT_DATA_PATH = './fixtures/projects.json'; const ORGANIZATION_DATA_PATH = './fixtures/organizations.json'; const USER_ORGANIZATION_DATA_PATH = './fixtures/user-orgnizations.json'; const PROJECT_MEMBER_DATA_PATH = './fixtures/project-members.json'; -const DOMAIN_DATA_PATH = './fixtures/domains.json'; +const PRIMARY_DOMAIN_DATA_PATH = './fixtures/primary-domains.json'; const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json'; const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json'; +const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json'; const loadAndSaveData = async (entityType: EntityTarget, dataSource: DataSource, filePath: string, relations?: any | undefined) => { const entitiesData = await fs.readFile(filePath, 'utf-8'); @@ -54,6 +55,8 @@ const loadAndSaveData = async (entityType: EntityT }; const generateTestData = async (dataSource: DataSource) => { + const domainRepository = await dataSource.getRepository(Domain); + const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH)); const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH)); @@ -64,11 +67,20 @@ const generateTestData = async (dataSource: DataSource) => { const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations); - const domainRelations = { + const domainPrimaryRelations = { project: savedProjects }; - const savedDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, DOMAIN_DATA_PATH), domainRelations); + const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations); + + const domainRedirectedRelations = { + project: savedProjects, + redirectTo: savedPrimaryDomains + }; + + await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations); + + const savedDomains = await domainRepository.find(); const userOrganizationRelations = { member: savedUsers, diff --git a/packages/frontend/src/components/projects/project/settings/Domains.tsx b/packages/frontend/src/components/projects/project/settings/Domains.tsx index e7f69a2..0286f54 100644 --- a/packages/frontend/src/components/projects/project/settings/Domains.tsx +++ b/packages/frontend/src/components/projects/project/settings/Domains.tsx @@ -22,11 +22,15 @@ const Domains = () => { }); }, [id, projects]); - const linkedRepo = useMemo(() => { - return currentProject?.repositories?.find( - (repo: any) => repo.id === Number(currentProject?.repositoryId), - ); - }, [currentProject]); + // TODO: Use github API for getting linked repository + const linkedRepo = { + id: 3, + title: 'project-103', + updatedAt: '2023-12-21T04:20:00', + user: 'charlie', + private: false, + branch: ['main', 'prod', 'test'], + }; const fetchDomains = async () => { if (currentProject === undefined) { diff --git a/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx b/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx index 3bd694a..f9dd6cc 100644 --- a/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx +++ b/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import toast from 'react-hot-toast'; import { Domain } from 'gql-client'; @@ -40,37 +40,57 @@ const EditDomainDialog = ({ const client = useGQLClient(); const getRedirectUrl = (domain: Domain) => { - const domainArr = domain.name.split('www.'); - let redirectUrl = ''; - if (domain.name.startsWith('www.')) { - redirectUrl = domainArr[1]; + const redirectDomain = domain.redirectTo; + + if (redirectDomain !== null) { + return redirectDomain?.name; } else { - redirectUrl = `www.${domainArr[0]}`; + return 'none'; } - return redirectUrl; }; const redirectOptions = useMemo(() => { - const redirectUrl = getRedirectUrl(domain); - return [...DEFAULT_REDIRECT_OPTIONS, redirectUrl]; - }, [domain]); + const domainNames = domains + .filter((domainData) => domainData.id !== domain.id) + .map((domain) => domain.name); + return ['none', ...domainNames]; + }, [domain, domains]); + + const domainRedirectedFrom = useMemo(() => { + return domains.find( + (domainData) => domainData.redirectTo?.id === domain.id, + ); + }, [domains, domain]); const isDisableDropdown = useMemo(() => { - const redirectUrl = getRedirectUrl(domain); + return domainRedirectedFrom?.redirectTo?.id !== undefined; + }, [domain, domains]); - const domainRedirected = domains.find( - (domain) => domain.name === redirectUrl, - ); + const { + handleSubmit, + register, + control, + watch, + reset, + formState: { isValid, isDirty }, + } = useForm({ + defaultValues: { + name: domain.name, + branch: domain.branch, + redirectedTo: getRedirectUrl(domain), + }, + }); - return domainRedirected?.isRedirected; - }, [domain]); - - const onSubmit = useCallback( + const updateDomainHandler = useCallback( async (data: any) => { + const domainRedirectTo = domains.find( + (domainData) => data.redirectedTo === domainData.name, + ); + const updates = { - name: data.name, - branch: data.branch, - isRedirected: data.redirectedTo !== 'none', + name: data.name ? data.name : domain.name, + branch: data.branch ? data.branch : domain.branch, + redirectToId: domainRedirectTo ? domainRedirectTo.id : null, }; const { updateDomain } = await client.updateDomain(domain.id, updates); @@ -79,29 +99,22 @@ const EditDomainDialog = ({ await onUpdate(); toast.success(`Domain ${domain.name} has been updated`); } else { + reset(); toast.error(`Error updating domain ${domain.name}`); } handleOpen(); }, - [client], + [client, domains, domain], ); - const { - handleSubmit, - register, - control, - watch, - formState: { isValid, isDirty }, - } = useForm({ - defaultValues: { + useEffect(() => { + reset({ name: domain.name, - branch: repo.branch[0], - redirectedTo: !domain.isRedirected - ? redirectOptions[0] - : redirectOptions[1], - }, - }); + branch: domain.branch, + redirectedTo: getRedirectUrl(domain), + }); + }, [domain, repo]); return ( @@ -115,7 +128,7 @@ const EditDomainDialog = ({ X -
+ Domain name @@ -137,8 +150,9 @@ const EditDomainDialog = ({
^
- Domain “{redirectOptions[1]}” redirects to this domain so you - can not redirect this doman further. + Domain “{domainRedirectedFrom ? domainRedirectedFrom.name : ''}” + redirects to this domain so you can not redirect this doman + further.
)} diff --git a/packages/frontend/src/layouts/ProjectSearch.tsx b/packages/frontend/src/layouts/ProjectSearch.tsx index 68ae1e7..5823f5c 100644 --- a/packages/frontend/src/layouts/ProjectSearch.tsx +++ b/packages/frontend/src/layouts/ProjectSearch.tsx @@ -41,7 +41,6 @@ const ProjectSearch = () => { name: '', value: '', }, - isRedirectedto: deployment.domain.isRedirected, } : null, }; diff --git a/packages/frontend/src/types/project.ts b/packages/frontend/src/types/project.ts index ff626bc..8409854 100644 --- a/packages/frontend/src/types/project.ts +++ b/packages/frontend/src/types/project.ts @@ -70,7 +70,6 @@ export interface DomainDetails { name: string; value: string; }; - isRedirectedto: boolean; } export enum Permission { diff --git a/packages/gql-client/src/queries.ts b/packages/gql-client/src/queries.ts index 4ce9eb3..0ccf502 100644 --- a/packages/gql-client/src/queries.ts +++ b/packages/gql-client/src/queries.ts @@ -48,7 +48,6 @@ query ($projectId: String!) { updatedAt id name - isRedirected } } } @@ -85,7 +84,6 @@ query ($organizationId: String!) { updatedAt id name - isRedirected } } } @@ -136,7 +134,6 @@ query ($projectId: String!) { domain{ branch createdAt - isRedirected id name status @@ -218,7 +215,12 @@ query ($projectId: String!) { domains(projectId: $projectId) { branch createdAt - isRedirected + redirectTo { + id + name + branch + status + } id name status diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index eeb8b40..47177aa 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -41,8 +41,8 @@ export type Domain = { id: string branch: string name: string - isRedirected: boolean status: DomainStatus + redirectTo?: Domain createdAt: string updatedAt: string } @@ -224,8 +224,8 @@ export type UpdateProjectInput = { export type UpdateDomainInput = { name?: string; - isRedirected?: boolean; branch?: string; + redirectToId?: string | null; } export type RedeployToProdResponse = {