Create new deployment on changing preview deployment to production (#61)

* Create new deployment when changing to production

* Remove unnecessary todos

* Move deployment id and url creation in database method

* Display correct details in deployment dialog box

* Rename relativeTime function to relativeTimeISO

* Refactor resolver methods to service class

* Refactor to move github app to service class

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-02-08 14:59:19 +05:30 committed by GitHub
parent e0001466e0
commit bd6a6b330c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 83 additions and 60 deletions

View File

@ -2,6 +2,8 @@ import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWh
import path from 'path'; import path from 'path';
import debug from 'debug'; import debug from 'debug';
import assert from 'assert'; import assert from 'assert';
import { customAlphabet } from 'nanoid';
import { lowercase, numbers } from 'nanoid-dictionary';
import { DatabaseConfig } from './config'; import { DatabaseConfig } from './config';
import { User } from './entity/User'; import { User } from './entity/User';
@ -15,6 +17,8 @@ import { PROJECT_DOMAIN } from './constants';
const log = debug('snowball:database'); const log = debug('snowball:database');
const nanoid = customAlphabet(lowercase + numbers, 8);
// TODO: Fix order of methods // TODO: Fix order of methods
export class Database { export class Database {
private dataSource: DataSource; private dataSource: DataSource;
@ -171,7 +175,16 @@ export class Database {
async addDeployement (data: DeepPartial<Deployment>): Promise<Deployment> { async addDeployement (data: DeepPartial<Deployment>): Promise<Deployment> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.save(data);
const id = nanoid();
const url = `${data.project!.name}-${id}.${PROJECT_DOMAIN}`;
const updatedData = {
...data,
id,
url
};
const deployment = await deploymentRepository.save(updatedData);
return deployment; return deployment;
} }

View File

@ -19,10 +19,6 @@ export const main = async (): Promise<void> => {
// TODO: get config path using cli // TODO: get config path using cli
const { server, database, githubOauth } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const { server, database, githubOauth } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const db = new Database(database);
await db.init();
const service = new Service(db);
// TODO: Move to Service class // TODO: Move to Service class
const app = new OAuthApp({ const app = new OAuthApp({
clientType: 'oauth-app', clientType: 'oauth-app',
@ -30,8 +26,12 @@ export const main = async (): Promise<void> => {
clientSecret: githubOauth.clientSecret clientSecret: githubOauth.clientSecret
}); });
const db = new Database(database);
await db.init();
const service = new Service(db, app);
const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString(); const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString();
const resolvers = await createResolvers(db, app, service); const resolvers = await createResolvers(service);
await createAndStartServer(typeDefs, resolvers, server); await createAndStartServer(typeDefs, resolvers, server);
}; };

View File

@ -1,10 +1,7 @@
import debug from 'debug'; import debug from 'debug';
import { DeepPartial, FindOptionsWhere } from 'typeorm'; import { DeepPartial, FindOptionsWhere } from 'typeorm';
import { OAuthApp } from '@octokit/oauth-app';
import { Service } from './service'; import { Service } from './service';
import { Database } from './database';
import { Permission } from './entity/ProjectMember'; import { Permission } from './entity/ProjectMember';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { Project } from './entity/Project'; import { Project } from './entity/Project';
@ -13,7 +10,7 @@ import { EnvironmentVariable } from './entity/EnvironmentVariable';
const log = debug('snowball:database'); const log = debug('snowball:database');
// TODO: Remove Database argument and refactor code to Service // TODO: Remove Database argument and refactor code to Service
export const createResolvers = async (db: Database, app: OAuthApp, service: Service): Promise<any> => { export const createResolvers = async (service: Service): Promise<any> => {
return { return {
Query: { Query: {
// TODO: add custom type for context // TODO: add custom type for context
@ -121,9 +118,9 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
} }
}, },
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }) => { updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
try { try {
return await service.updateDeploymentToProd(deploymentId); return Boolean(await service.updateDeploymentToProd(context.userId, deploymentId));
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@ -201,19 +198,17 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
}, },
authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => { authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => {
// TOO: Move to Service class try {
const { authentication: { token } } = await app.createToken({ return await service.authenticateGitHub(code, context.userId);
code } catch (err) {
}); log(err);
return false;
await db.updateUser(context.userId, { gitHubToken: token }); }
return { token };
}, },
unauthenticateGitHub: async (_: any, __: object, context: any) => { unauthenticateGitHub: async (_: any, __: object, context: any) => {
try { try {
return db.updateUser(context.userId, { gitHubToken: null }); return service.unauthenticateGitHub(context.userId, { gitHubToken: null });
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;

View File

@ -1,8 +1,8 @@
import assert from 'assert'; import assert from 'assert';
import { customAlphabet } from 'nanoid';
import { lowercase, numbers } from 'nanoid-dictionary';
import { DeepPartial, FindOptionsWhere } from 'typeorm'; import { DeepPartial, FindOptionsWhere } from 'typeorm';
import { OAuthApp } from '@octokit/oauth-app';
import { Database } from './database'; import { Database } from './database';
import { Deployment, Environment } from './entity/Deployment'; import { Deployment, Environment } from './entity/Deployment';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
@ -11,15 +11,13 @@ import { Organization } from './entity/Organization';
import { Project } from './entity/Project'; import { Project } from './entity/Project';
import { Permission, ProjectMember } from './entity/ProjectMember'; import { Permission, ProjectMember } from './entity/ProjectMember';
import { User } from './entity/User'; import { User } from './entity/User';
import { PROJECT_DOMAIN } from './constants';
const nanoid = customAlphabet(lowercase + numbers, 8);
export class Service { export class Service {
private db: Database; private db: Database;
private app: OAuthApp;
constructor (db: Database) { constructor (db: Database, app: OAuthApp) {
this.db = db; this.db = db;
this.app = app;
} }
async getUser (userId: string): Promise<User | null> { async getUser (userId: string): Promise<User | null> {
@ -149,7 +147,7 @@ export class Service {
return this.db.deleteEnvironmentVariable(environmentVariableId); return this.db.deleteEnvironmentVariable(environmentVariableId);
} }
async updateDeploymentToProd (deploymentId: string): Promise<boolean> { async updateDeploymentToProd (userId: string, deploymentId: string): Promise<Deployment> {
const deployment = await this.db.getDeployment({ where: { id: deploymentId }, relations: { project: true } }); const deployment = await this.db.getDeployment({ where: { id: deploymentId }, relations: { project: true } });
if (!deployment) { if (!deployment) {
@ -173,13 +171,18 @@ export class Service {
}); });
} }
const updateResult = await this.db.updateDeploymentById(deploymentId, { const { createdAt, updatedAt, ...updatedDeployment } = deployment;
environment: Environment.Production,
domain: prodBranchDomains[0], updatedDeployment.isCurrent = true;
isCurrent: true updatedDeployment.environment = Environment.Production;
updatedDeployment.domain = prodBranchDomains[0];
updatedDeployment.createdBy = Object.assign(new User(), {
id: userId
}); });
return updateResult; const newDeployement = await this.db.addDeployement(updatedDeployment);
return newDeployement;
} }
async addProject (userId: string, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> { async addProject (userId: string, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
@ -243,9 +246,6 @@ export class Service {
}); });
} }
updatedDeployment.id = nanoid();
updatedDeployment.url = `${updatedDeployment.project.name}-${updatedDeployment.id}.${PROJECT_DOMAIN}`;
const oldDeployment = await this.db.updateDeploymentById(deploymentId, { domain: null, isCurrent: false }); const oldDeployment = await this.db.updateDeploymentById(deploymentId, { domain: null, isCurrent: false });
const newDeployement = await this.db.addDeployement(updatedDeployment); const newDeployement = await this.db.addDeployement(updatedDeployment);
@ -360,4 +360,18 @@ export class Service {
return updateResult; return updateResult;
} }
async authenticateGitHub (code:string, userId: string): Promise<{token: string}> {
const { authentication: { token } } = await this.app.createToken({
code
});
await this.db.updateUser(userId, { gitHubToken: token });
return { token };
}
async unauthenticateGitHub (userId: string, data: DeepPartial<User>): Promise<boolean> {
return this.db.updateUser(userId, data);
}
} }

View File

@ -2,11 +2,11 @@
{ {
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26", "id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
"name": "Snowball Tools", "name": "Snowball Tools",
"slug": "snowball-tools" "slug": "snowball-tools-1"
}, },
{ {
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb", "id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
"name": "AirFoil", "name": "AirFoil",
"slug": "airfoil" "slug": "airfoil-2"
} }
] ]

View File

@ -9,7 +9,7 @@ import {
Typography, Typography,
} from '@material-tailwind/react'; } from '@material-tailwind/react';
import { relativeTime } from '../../utils/time'; import { relativeTimeISO } from '../../utils/time';
import { ProjectDetails } from '../../types/project'; import { ProjectDetails } from '../../types/project';
interface ProjectCardProps { interface ProjectCardProps {
@ -45,7 +45,7 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
{project.latestCommit.message} {project.latestCommit.message}
</Typography> </Typography>
<Typography variant="small" color="gray"> <Typography variant="small" color="gray">
{relativeTime(project.latestCommit.createdAt)} on{' '} {relativeTimeISO(project.latestCommit.createdAt)} on{' '}
{project.latestCommit.branch} {project.latestCommit.branch}
</Typography> </Typography>
</div> </div>

View File

@ -3,7 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { Chip, IconButton } from '@material-tailwind/react'; import { Chip, IconButton } from '@material-tailwind/react';
import { relativeTime } from '../../../utils/time'; import { relativeTimeISO } from '../../../utils/time';
import { GitRepositoryDetails } from '../../../types/project'; import { GitRepositoryDetails } from '../../../types/project';
import { useGQLClient } from '../../../context/GQLClientContext'; import { useGQLClient } from '../../../context/GQLClientContext';
@ -24,7 +24,6 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
const { addProject } = await client.addProject(orgSlug!, { const { addProject } = await client.addProject(orgSlug!, {
name: `${repository.owner!.login}-${repository.name}`, name: `${repository.owner!.login}-${repository.name}`,
// TODO: Get organization id from context or URL
prodBranch: repository.default_branch!, prodBranch: repository.default_branch!,
repository: repository.full_name, repository: repository.full_name,
}); });
@ -50,7 +49,7 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
/> />
)} )}
</div> </div>
<p>{repository.updated_at && relativeTime(repository.updated_at)}</p> <p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
</div> </div>
<div className="hidden group-hover:block"> <div className="hidden group-hover:block">
<IconButton size="sm">{'>'}</IconButton> <IconButton size="sm">{'>'}</IconButton>

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Typography, IconButton } from '@material-tailwind/react'; import { Typography, IconButton } from '@material-tailwind/react';
import { relativeTime } from '../../../utils/time'; import { relativeTimeISO } from '../../../utils/time';
import { GitCommitDetails } from '../../../types/project'; import { GitCommitDetails } from '../../../types/project';
interface ActivityCardProps { interface ActivityCardProps {
@ -17,7 +17,8 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
<div className="grow"> <div className="grow">
<Typography>{activity.commit.author?.name}</Typography> <Typography>{activity.commit.author?.name}</Typography>
<Typography variant="small" color="gray"> <Typography variant="small" color="gray">
{relativeTime(activity.commit.author!.date!)} ^ {activity.branch.name} {relativeTimeISO(activity.commit.author!.date!)} ^{' '}
{activity.branch.name}
</Typography> </Typography>
<Typography variant="small" color="gray"> <Typography variant="small" color="gray">
{activity.commit.message} {activity.commit.message}

View File

@ -3,7 +3,7 @@ import React from 'react';
import { Typography, Chip, Card } from '@material-tailwind/react'; import { Typography, Chip, Card } from '@material-tailwind/react';
import { color } from '@material-tailwind/react/types/components/chip'; import { color } from '@material-tailwind/react/types/components/chip';
import { DeploymentDetails } from '../../../../types/project'; import { DeploymentDetails } from '../../../../types/project';
import { relativeTime } from '../../../../utils/time'; import { relativeTimeMs } from '../../../../utils/time';
interface DeploymentDialogBodyCardProps { interface DeploymentDialogBodyCardProps {
deployment: DeploymentDetails; deployment: DeploymentDetails;
@ -28,14 +28,14 @@ const DeploymentDialogBodyCard = ({
/> />
)} )}
<Typography variant="small" className="text-black"> <Typography variant="small" className="text-black">
{deployment.title} {deployment.url}
</Typography> </Typography>
<Typography variant="small"> <Typography variant="small">
^ {deployment.branch} ^ {deployment.commitHash}{' '} ^ {deployment.branch} ^ {deployment.commitHash}{' '}
{deployment.commit.message} {deployment.commit.message}
</Typography> </Typography>
<Typography variant="small"> <Typography variant="small">
^ {relativeTime(deployment.updatedAt)} ^ {deployment.author} ^ {relativeTimeMs(deployment.createdAt)} ^ {deployment.createdBy.name}
</Typography> </Typography>
</Card> </Card>
); );

View File

@ -5,3 +5,6 @@ export const COMMIT_DETAILS = {
}; };
export const ORGANIZATION_ID = '2379cf1f-a232-4ad2-ae14-4d881131cc26'; export const ORGANIZATION_ID = '2379cf1f-a232-4ad2-ae14-4d881131cc26';
export const GIT_TEMPLATE_LINK =
'https://git.vdb.to/cerc-io/test-progressive-web-app';

View File

@ -3,6 +3,7 @@ import { Outlet, useLocation, useSearchParams } from 'react-router-dom';
import Stepper from '../../../../components/Stepper'; import Stepper from '../../../../components/Stepper';
import templateDetails from '../../../../assets/templates.json'; import templateDetails from '../../../../assets/templates.json';
import { GIT_TEMPLATE_LINK } from '../../../../constants';
const STEPPER_VALUES = [ const STEPPER_VALUES = [
{ step: 1, route: '/projects/create/template', label: 'Create repository' }, { step: 1, route: '/projects/create/template', label: 'Create repository' },
@ -31,8 +32,11 @@ const CreateWithTemplate = () => {
<div className="flex justify-between w-5/6 my-4 bg-gray-200 rounded-xl p-6"> <div className="flex justify-between w-5/6 my-4 bg-gray-200 rounded-xl p-6">
<div>^</div> <div>^</div>
<div className="grow">{template?.name}</div> <div className="grow">{template?.name}</div>
{/* TODO: Get template Git link from DB */} <div>
<div>^snowball-tools/react-native-starter</div> <a href={GIT_TEMPLATE_LINK} target="_blank" rel="noreferrer">
cerc-io/test-progressive-web-app
</a>
</div>
</div> </div>
<div className="grid grid-cols-3 w-5/6 p-6"> <div className="grid grid-cols-3 w-5/6 p-6">
<div> <div>

View File

@ -50,7 +50,6 @@ const CreateRepo = () => {
const { addProject } = await client.addProject(orgSlug!, { const { addProject } = await client.addProject(orgSlug!, {
name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`, name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`,
// TODO: Get organization id from context or URL
prodBranch: gitRepo.data.default_branch ?? 'main', prodBranch: gitRepo.data.default_branch ?? 'main',
repository: gitRepo.data.full_name, repository: gitRepo.data.full_name,
}); });
@ -95,8 +94,6 @@ const CreateRepo = () => {
} }
}, [gitAccounts]); }, [gitAccounts]);
// TODO: Get users and orgs from GitHub
return ( return (
<form onSubmit={handleSubmit(submitRepoHandler)}> <form onSubmit={handleSubmit(submitRepoHandler)}>
<div className="mb-2"> <div className="mb-2">
@ -135,10 +132,7 @@ const CreateRepo = () => {
name="account" name="account"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<AsyncSelect <AsyncSelect {...field}>
{...field}
label={!field.value ? 'Select an account / Organization' : ''}
>
{gitAccounts.map((account, key) => ( {gitAccounts.map((account, key) => (
<Option key={key} value={account}> <Option key={key} value={account}>
^ {account} ^ {account}

View File

@ -6,7 +6,7 @@ import { DateTime } from 'luxon';
* @param {string} time - The input time in ISO 8601 format. * @param {string} time - The input time in ISO 8601 format.
* @returns {string} - A human-readable relative time string. * @returns {string} - A human-readable relative time string.
*/ */
export const relativeTime = (time: string) => { export const relativeTimeISO = (time: string) => {
return DateTime.fromISO(time).toRelative(); return DateTime.fromISO(time).toRelative();
}; };
@ -17,5 +17,5 @@ export const relativeTime = (time: string) => {
* @returns {string} - A human-readable relative time string. * @returns {string} - A human-readable relative time string.
*/ */
export const relativeTimeMs = (time: string) => { export const relativeTimeMs = (time: string) => {
return relativeTime(new Date(Number(time)).toISOString()); return relativeTimeISO(new Date(Number(time)).toISOString());
}; };