Implement functionality for editing domain (#57)

* Make use of domain id for redirecting domains

* Add checks in backend to prevent chain redirecting and editing redirected domain

* Add self relation to domain entity and use it to create and edit domains

* Add self referencing relation to initialize db script

* Add redirectToId column in domain entity

* Remove isRedirected flag from domain entity

* Refactor edit domain dialog box

* Use dummy data for repository

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-01-31 17:55:19 +05:30 committed by Ashwin Phatak
parent ace27c7eae
commit 0dd6c7702a
13 changed files with 177 additions and 94 deletions

View File

@ -378,24 +378,22 @@ export class Database {
const primaryDomainDetails = {
...domainDetails,
isRedirected: false,
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]}`,
isRedirected: true,
branch: currentProject.prodBranch,
project: currentProject
project: currentProject,
redirectTo: savedPrimaryDomain
};
const savedPrimaryDomain = await domainRepository.save(primaryDomain);
const redirectedDomain = domainRepository.create(redirectedDomainDetails as DeepPartial<Domain>);
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<Domain>): Promise<boolean> {
async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
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<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;

View File

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

View File

@ -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<any>
}
},
updateDomain: async (_: any, { domainId, domainDetails }: { domainId: string, domainDetails: {name?: string, isRedirected?: boolean, branch?: string }}) => {
updateDomain: async (_: any, { domainId, domainDetails }: { domainId: string, domainDetails: DeepPartial<Domain>}) => {
try {
await db.updateDomainById(domainId, domainDetails);
return true;

View File

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

View File

@ -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"
}
]

View File

@ -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"
}
]

View File

@ -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 <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, filePath: string, relations?: any | undefined) => {
const entitiesData = await fs.readFile(filePath, 'utf-8');
@ -54,6 +55,8 @@ const loadAndSaveData = async <Entity extends ObjectLiteral>(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,

View File

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

View File

@ -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 (
<Dialog open={open} handler={handleOpen}>
@ -115,7 +128,7 @@ const EditDomainDialog = ({
X
</Button>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(updateDomainHandler)}>
<DialogBody className="flex flex-col gap-2 p-4">
<Typography variant="small">Domain name</Typography>
<Input crossOrigin={undefined} {...register('name')} />
@ -137,8 +150,9 @@ const EditDomainDialog = ({
<div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg">
<div>^</div>
<Typography variant="small">
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.
</Typography>
</div>
)}

View File

@ -41,7 +41,6 @@ const ProjectSearch = () => {
name: '',
value: '',
},
isRedirectedto: deployment.domain.isRedirected,
}
: null,
};

View File

@ -70,7 +70,6 @@ export interface DomainDetails {
name: string;
value: string;
};
isRedirectedto: boolean;
}
export enum Permission {

View File

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

View File

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