snowballtools-base/packages/backend/src/service.ts
Vivian Phung a684743bd6
feat(template projects): generate git repo on backend (#218)
### TL;DR

- Still cretaes app if user migrates from page

The PR introduces a new `AddProjectFromTemplate` mutation to facilitate project creation using a repository template. This change centralizes the template project creation logic within the backend, improving code maintainability by removing redundant client-side code.

### What changed?
- Added `AddProjectFromTemplate` input type in `schema.gql` and corresponding TypeScript interfaces.
- Implemented `addProjectFromTemplate` resolver with error handling and Octokit integration for repository creation.
- Updated `service.ts` to include the new `addProjectFromTemplate` method.
- Created new GraphQL `Mutation` for `addProjectFromTemplate` in the GraphQL schema.
- Adjusted the client-side GQLClient to support the new mutation.
- Modified frontend to utilize the new backend mutation for project creation from a template.

### How to test?
1. Ensure your backend server is running.
2. Use a GraphQL client like Postman to call the `addProjectFromTemplate` mutation with appropriate input.
3. Verify that the new project is created using the specified template, and appropriate error messages are returned for failures.
4. Check the frontend flow for creating a project from a template to ensure it is working correctly.

### Why make this change?
This change enhances code maintainability by centralizing template project creation logic within the backend, thereby reducing redundancy and potential inconsistencies in client-side implementations.

---
2024-06-24 18:38:01 -04:00

1081 lines
30 KiB
TypeScript

import assert from 'assert';
import debug from 'debug';
import { DeepPartial, FindOptionsWhere } from 'typeorm';
import { Octokit, RequestError } from 'octokit';
import { OAuthApp } from '@octokit/oauth-app';
import { Database } from './database';
import { Deployment, DeploymentStatus, Environment } from './entity/Deployment';
import { Domain } from './entity/Domain';
import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { Organization } from './entity/Organization';
import { Project } from './entity/Project';
import { Permission, ProjectMember } from './entity/ProjectMember';
import { User } from './entity/User';
import { Registry } from './registry';
import { GitHubConfig, RegistryConfig } from './config';
import {
AddProjectFromTemplateInput,
AppDeploymentRecord,
AppDeploymentRemovalRecord,
GitPushEventPayload,
PackageJSON,
} from './types';
import { Role } from './entity/UserOrganization';
const log = debug('snowball:service');
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
// Define a constant for an hour in milliseconds
const HOUR = 1000 * 60 * 60;
interface Config {
gitHubConfig: GitHubConfig;
registryConfig: RegistryConfig;
}
export class Service {
private db: Database;
private oauthApp: OAuthApp;
private registry: Registry;
private config: Config;
private deployRecordCheckTimeout?: NodeJS.Timeout;
constructor(config: Config, db: Database, app: OAuthApp, registry: Registry) {
this.db = db;
this.oauthApp = app;
this.registry = registry;
this.config = config;
this.init();
}
/**
* Initialize services
*/
init(): void {
// Start check for ApplicationDeploymentRecords asynchronously
this.checkDeployRecordsAndUpdate();
// Start check for ApplicationDeploymentRemovalRecords asynchronously
this.checkDeploymentRemovalRecordsAndUpdate();
}
/**
* Destroy services
*/
destroy(): void {
clearTimeout(this.deployRecordCheckTimeout);
}
/**
* Checks for ApplicationDeploymentRecord and update corresponding deployments
* Continues check in loop after a delay of registryConfig.fetchDeploymentRecordDelay
*/
async checkDeployRecordsAndUpdate(): Promise<void> {
// Fetch deployments in building state
const deployments = await this.db.getDeployments({
where: {
status: DeploymentStatus.Building,
},
});
if (deployments.length) {
log(
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`,
);
// Calculate a timestamp for one hour ago
const anHourAgo = Date.now() - HOUR;
// Filter out deployments started more than an hour ago and mark them as Error
const oldDeploymentsToUpdate = deployments
.filter((deployment) => Number(deployment.updatedAt) < anHourAgo)
.map((deployment) => {
return this.db.updateDeploymentById(deployment.id, {
status: DeploymentStatus.Error,
isCurrent: false,
});
});
// If there are old deployments to update, log and perform the updates
if (oldDeploymentsToUpdate.length > 0) {
log(
`Cleaning up ${oldDeploymentsToUpdate.length} deployments stuck in ${DeploymentStatus.Building} state for over an hour`,
);
await Promise.all(oldDeploymentsToUpdate);
}
// Fetch ApplicationDeploymentRecord for deployments
const records = await this.registry.getDeploymentRecords(deployments);
log(`Found ${records.length} ApplicationDeploymentRecords`);
// Update deployments for which ApplicationDeploymentRecords were returned
if (records.length) {
await this.updateDeploymentsWithRecordData(records);
}
}
this.deployRecordCheckTimeout = setTimeout(() => {
this.checkDeployRecordsAndUpdate();
}, this.config.registryConfig.fetchDeploymentRecordDelay);
}
/**
* Checks for ApplicationDeploymentRemovalRecord and remove corresponding deployments
* Continues check in loop after a delay of registryConfig.fetchDeploymentRecordDelay
*/
async checkDeploymentRemovalRecordsAndUpdate(): Promise<void> {
// Fetch deployments in deleting state
const deployments = await this.db.getDeployments({
where: {
status: DeploymentStatus.Deleting,
},
});
if (deployments.length) {
log(
`Found ${deployments.length} deployments in ${DeploymentStatus.Deleting} state`,
);
// Fetch ApplicationDeploymentRemovalRecords for deployments
const records =
await this.registry.getDeploymentRemovalRecords(deployments);
log(`Found ${records.length} ApplicationDeploymentRemovalRecords`);
// Update deployments for which ApplicationDeploymentRemovalRecords were returned
if (records.length) {
await this.deleteDeploymentsWithRecordData(records, deployments);
}
}
this.deployRecordCheckTimeout = setTimeout(() => {
this.checkDeploymentRemovalRecordsAndUpdate();
}, this.config.registryConfig.fetchDeploymentRecordDelay);
}
/**
* Update deployments with ApplicationDeploymentRecord data
*/
async updateDeploymentsWithRecordData(
records: AppDeploymentRecord[],
): Promise<void> {
// Get deployments for ApplicationDeploymentRecords
const deployments = await this.db.getDeployments({
where: records.map((record) => ({
applicationRecordId: record.attributes.application,
})),
order: {
createdAt: 'DESC',
},
});
// Get project IDs of deployments that are in production environment
const productionDeploymentProjectIds = deployments.reduce(
(acc, deployment): Set<string> => {
if (deployment.environment === Environment.Production) {
acc.add(deployment.projectId);
}
return acc;
},
new Set<string>(),
);
// Set old deployments isCurrent to false
await this.db.updateDeploymentsByProjectIds(
Array.from(productionDeploymentProjectIds),
{ isCurrent: false },
);
const recordToDeploymentsMap = deployments.reduce(
(acc: { [key: string]: Deployment }, deployment) => {
acc[deployment.applicationRecordId] = deployment;
return acc;
},
{},
);
// Update deployment data for ApplicationDeploymentRecords
const deploymentUpdatePromises = records.map(async (record) => {
const deployment = recordToDeploymentsMap[record.attributes.application];
await this.db.updateDeploymentById(deployment.id, {
applicationDeploymentRecordId: record.id,
applicationDeploymentRecordData: record.attributes,
url: record.attributes.url,
status: DeploymentStatus.Ready,
isCurrent: deployment.environment === Environment.Production,
});
log(
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`,
);
});
await Promise.all(deploymentUpdatePromises);
}
/**
* Delete deployments with ApplicationDeploymentRemovalRecord data
*/
async deleteDeploymentsWithRecordData(
records: AppDeploymentRemovalRecord[],
deployments: Deployment[],
): Promise<void> {
const removedApplicationDeploymentRecordIds = records.map(
(record) => record.attributes.deployment,
);
// Get removed deployments for ApplicationDeploymentRecords
const removedDeployments = deployments.filter((deployment) =>
removedApplicationDeploymentRecordIds.includes(
deployment.applicationDeploymentRecordId!,
),
);
const recordToDeploymentsMap = removedDeployments.reduce(
(acc: { [key: string]: Deployment }, deployment) => {
acc[deployment.applicationDeploymentRecordId!] = deployment;
return acc;
},
{},
);
// Update deployment data for ApplicationDeploymentRecords and delete
const deploymentUpdatePromises = records.map(async (record) => {
const deployment = recordToDeploymentsMap[record.attributes.deployment];
await this.db.updateDeploymentById(deployment.id, {
applicationDeploymentRemovalRecordId: record.id,
applicationDeploymentRemovalRecordData: record.attributes,
});
log(
`Updated deployment ${deployment.id} with ApplicationDeploymentRemovalRecord ${record.id}`,
);
await this.db.deleteDeploymentById(deployment.id);
});
await Promise.all(deploymentUpdatePromises);
}
async getUser(userId: string): Promise<User | null> {
return this.db.getUser({
where: {
id: userId,
},
});
}
async getUserByEmail(email: string): Promise<User | null> {
return await this.db.getUser({
where: {
email,
},
});
}
async getUserBySubOrgId(subOrgId: string): Promise<User | null> {
return await this.db.getUser({
where: {
subOrgId,
},
});
}
async getUserByEthAddress(ethAddress: string): Promise<User | null> {
return await this.db.getUser({
where: {
ethAddress,
},
});
}
async createUser(params: {
name: string;
email: string;
subOrgId: string;
ethAddress: string;
turnkeyWalletId: string;
}): Promise<User> {
const [org] = await this.db.getOrganizations({});
assert(org, 'No organizations exists in database');
// Create user with new address
const user = await this.db.addUser({
email: params.email,
name: params.name,
subOrgId: params.subOrgId,
ethAddress: params.ethAddress,
isVerified: true,
turnkeyWalletId: params.turnkeyWalletId,
});
await this.db.addUserOrganization({
member: user,
organization: org,
role: Role.Owner,
});
return user;
}
async getOctokit(userId: string): Promise<Octokit> {
const user = await this.db.getUser({ where: { id: userId } });
assert(
user && user.gitHubToken,
'User needs to be authenticated with GitHub token',
);
return new Octokit({ auth: user.gitHubToken });
}
async getOrganizationsByUserId(user: User): Promise<Organization[]> {
const dbOrganizations = await this.db.getOrganizationsByUserId(user.id);
return dbOrganizations;
}
async getProjectById(projectId: string): Promise<Project | null> {
const dbProject = await this.db.getProjectById(projectId);
return dbProject;
}
async getProjectsInOrganization(
user: User,
organizationSlug: string,
): Promise<Project[]> {
const dbProjects = await this.db.getProjectsInOrganization(
user.id,
organizationSlug,
);
return dbProjects;
}
async getDeploymentsByProjectId(projectId: string): Promise<Deployment[]> {
const dbDeployments = await this.db.getDeploymentsByProjectId(projectId);
return dbDeployments;
}
async getEnvironmentVariablesByProjectId(
projectId: string,
): Promise<EnvironmentVariable[]> {
const dbEnvironmentVariables =
await this.db.getEnvironmentVariablesByProjectId(projectId);
return dbEnvironmentVariables;
}
async getProjectMembersByProjectId(
projectId: string,
): Promise<ProjectMember[]> {
const dbProjectMembers =
await this.db.getProjectMembersByProjectId(projectId);
return dbProjectMembers;
}
async searchProjects(user: User, searchText: string): Promise<Project[]> {
const dbProjects = await this.db.getProjectsBySearchText(
user.id,
searchText,
);
return dbProjects;
}
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> {
return this.db.updateProjectMemberById(projectMemberId, data);
}
async addProjectMember(
projectId: string,
data: {
email: string;
permissions: Permission[];
},
): Promise<ProjectMember> {
// TODO: Send invitation
let user = await this.db.getUser({
where: {
email: data.email,
},
});
if (!user) {
user = await this.db.addUser({
email: data.email,
});
}
const newProjectMember = await this.db.addProjectMember({
project: {
id: projectId,
},
permissions: data.permissions,
isPending: true,
member: {
id: user.id,
},
});
return newProjectMember;
}
async removeProjectMember(
user: User,
projectMemberId: string,
): Promise<boolean> {
const member = await this.db.getProjectMemberById(projectMemberId);
if (String(member.member.id) === user.id) {
throw new Error('Invalid operation: cannot remove self');
}
const memberProject = member.project;
assert(memberProject);
if (String(user.id) === 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<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();
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
formattedEnvironmentVariables,
);
return savedEnvironmentVariables;
}
async updateEnvironmentVariable(
environmentVariableId: string,
data: DeepPartial<EnvironmentVariable>,
): Promise<boolean> {
return this.db.updateEnvironmentVariable(environmentVariableId, data);
}
async removeEnvironmentVariable(
environmentVariableId: string,
): Promise<boolean> {
return this.db.deleteEnvironmentVariable(environmentVariableId);
}
async updateDeploymentToProd(
user: User,
deploymentId: string,
): Promise<Deployment> {
const oldDeployment = await this.db.getDeployment({
where: { id: deploymentId },
relations: {
project: true,
},
});
if (!oldDeployment) {
throw new Error('Deployment does not exist');
}
const prodBranchDomains = await this.db.getDomainsByProjectId(
oldDeployment.project.id,
{ branch: oldDeployment.project.prodBranch },
);
const octokit = await this.getOctokit(user.id);
const newDeployment = await this.createDeployment(user.id, octokit, {
project: oldDeployment.project,
branch: oldDeployment.branch,
environment: Environment.Production,
domain: prodBranchDomains[0],
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage,
});
return newDeployment;
}
async createDeployment(
userId: string,
octokit: Octokit,
data: DeepPartial<Deployment>,
): Promise<Deployment> {
assert(data.project?.repository, 'Project repository not found');
log(
`Creating deployment in project ${data.project.name} from branch ${data.branch}`,
);
const [owner, repo] = data.project.repository.split('/');
const { data: packageJSONData } = await octokit.rest.repos.getContent({
owner,
repo,
path: 'package.json',
ref: data.commitHash,
});
if (!packageJSONData) {
throw new Error('Package.json file not found');
}
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
assert(packageJSON.name, "name field doesn't exist in package.json");
const repoUrl = (
await octokit.rest.repos.get({
owner,
repo,
})
).data.html_url;
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
const { applicationRecordId, applicationRecordData } =
await this.registry.createApplicationRecord({
appName: repo,
packageJSON,
appType: data.project!.template!,
commitHash: data.commitHash!,
repoUrl,
});
// Update previous deployment with prod branch domain
// TODO: Fix unique constraint error for domain
if (data.domain) {
await this.db.updateDeployment(
{
domainId: data.domain.id,
},
{
domain: null,
},
);
}
const newDeployment = await this.db.addDeployment({
project: data.project,
branch: data.branch,
commitHash: data.commitHash,
commitMessage: data.commitMessage,
environment: data.environment,
status: DeploymentStatus.Building,
applicationRecordId,
applicationRecordData,
domain: data.domain,
createdBy: Object.assign(new User(), {
id: userId,
}),
});
log(
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`,
);
const environmentVariables =
await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
environment: Environment.Production,
});
const environmentVariablesObj = environmentVariables.reduce(
(acc, env) => {
acc[env.key] = env.value;
return acc;
},
{} as { [key: string]: string },
);
// To set project DNS
if (data.environment === Environment.Production) {
// On deleting deployment later, project DNS deployment is also deleted
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
await this.registry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`,
});
}
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
await this.registry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`,
});
await this.db.updateDeploymentById(newDeployment.id, {
applicationDeploymentRequestId,
applicationDeploymentRequestData,
});
return newDeployment;
}
async addProjectFromTemplate(
user: User,
organizationSlug: string,
data: AddProjectFromTemplateInput,
): Promise<Project | undefined> {
try {
const octokit = await this.getOctokit(user.id);
const gitRepo = await octokit?.rest.repos.createUsingTemplate({
template_owner: data.templateOwner,
template_repo: data.templateRepo,
owner: data.owner,
name: data.name,
include_all_branches: false,
private: data.isPrivate,
});
if (!gitRepo) {
throw new Error('Failed to create repository from template');
}
const createdTemplateRepo = await octokit.rest.repos.get({
owner: data.owner,
repo: data.name,
});
const prodBranch = createdTemplateRepo.data.default_branch ?? 'main';
const project = await this.addProject(user, organizationSlug, {
name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`,
prodBranch,
repository: gitRepo.data.full_name,
// TODO: Set selected template
template: 'webapp',
});
if (!project || !project.id) {
throw new Error('Failed to create project from template');
}
return project;
} catch (error) {
console.error('Error creating project from template:', error);
throw error;
}
}
async addProject(
user: User,
organizationSlug: string,
data: DeepPartial<Project>,
): Promise<Project | undefined> {
const organization = await this.db.getOrganization({
where: {
slug: organizationSlug,
},
});
if (!organization) {
throw new Error('Organization does not exist');
}
const project = await this.db.addProject(user, organization.id, data);
const octokit = await this.getOctokit(user.id);
const [owner, repo] = project.repository.split('/');
const {
data: [latestCommit],
} = await octokit.rest.repos.listCommits({
owner,
repo,
sha: project.prodBranch,
per_page: 1,
});
// Create deployment with prod branch and latest commit
const deployment = await this.createDeployment(user.id, octokit, {
project,
branch: project.prodBranch,
environment: Environment.Production,
domain: null,
commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message,
});
await this.createRepoHook(octokit, project);
console.log('projectid is', project.id);
return project;
}
async createRepoHook(octokit: Octokit, project: Project): Promise<void> {
try {
const [owner, repo] = project.repository.split('/');
await octokit.rest.repos.createWebhook({
owner,
repo,
config: {
url: new URL(
'api/github/webhook',
this.config.gitHubConfig.webhookUrl,
).href,
content_type: 'json',
},
events: ['push'],
});
} catch (err) {
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
if (
!(
err instanceof RequestError &&
err.status === 422 &&
(err.response?.data as any).errors.some(
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR,
)
)
) {
throw err;
}
log(GITHUB_UNIQUE_WEBHOOK_ERROR);
}
}
async handleGitHubPush(data: GitPushEventPayload): Promise<void> {
const { repository, ref, head_commit: headCommit, deleted } = data;
if (deleted) {
log(`Branch ${ref} deleted for project ${repository.full_name}`);
return;
}
log(
`Handling GitHub push event from repository: ${repository.full_name}, branch: ${ref}`,
);
const projects = await this.db.getProjects({
where: { repository: repository.full_name },
});
if (!projects.length) {
log(`No projects found for repository ${repository.full_name}`);
}
// The `ref` property contains the full reference, including the branch name
// For example, "refs/heads/main" or "refs/heads/feature-branch"
const branch = ref.split('/').pop();
for await (const project of projects) {
const octokit = await this.getOctokit(project.ownerId);
const [domain] = await this.db.getDomainsByProjectId(project.id, {
branch,
});
// Create deployment with branch and latest commit in GitHub data
await this.createDeployment(project.ownerId, octokit, {
project,
branch,
environment:
project.prodBranch === branch
? Environment.Production
: Environment.Preview,
domain,
commitHash: headCommit.id,
commitMessage: headCommit.message,
});
}
}
async updateProject(
projectId: string,
data: DeepPartial<Project>,
): Promise<boolean> {
return this.db.updateProjectById(projectId, data);
}
async deleteProject(projectId: string): Promise<boolean> {
// TODO: Remove GitHub repo hook
return this.db.deleteProjectById(projectId);
}
async deleteDomain(domainId: string): Promise<boolean> {
const domainsRedirectedFrom = await this.db.getDomains({
where: {
redirectToId: domainId,
},
});
if (domainsRedirectedFrom.length > 0) {
throw new Error(
'Cannot delete domain since it has redirects from other domains',
);
}
return this.db.deleteDomainById(domainId);
}
async redeployToProd(user: User, deploymentId: string): Promise<Deployment> {
const oldDeployment = await this.db.getDeployment({
relations: {
project: true,
domain: true,
createdBy: true,
},
where: {
id: deploymentId,
},
});
if (oldDeployment === null) {
throw new Error('Deployment not found');
}
const octokit = await this.getOctokit(user.id);
const newDeployment = await this.createDeployment(user.id, octokit, {
project: oldDeployment.project,
// TODO: Put isCurrent field in project
branch: oldDeployment.branch,
environment: Environment.Production,
domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage,
});
return newDeployment;
}
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 deployment 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 deleteDeployment(deploymentId: string): Promise<boolean> {
const deployment = await this.db.getDeployment({
where: {
id: deploymentId,
},
relations: {
project: true,
},
});
if (deployment && deployment.applicationDeploymentRecordId) {
// If deployment is current, remove deployment for project subdomain as well
if (deployment.isCurrent) {
const currentDeploymentURL = `https://${deployment.project.subDomain}`;
const deploymentRecords =
await this.registry.getDeploymentRecordsByFilter({
application: deployment.applicationRecordId,
url: currentDeploymentURL,
});
if (!deploymentRecords.length) {
log(
`No ApplicationDeploymentRecord found for URL ${currentDeploymentURL} and ApplicationDeploymentRecord id ${deployment.applicationDeploymentRecordId}`,
);
return false;
}
await this.registry.createApplicationDeploymentRemovalRequest({
deploymentId: deploymentRecords[0].id,
});
}
const result =
await this.registry.createApplicationDeploymentRemovalRequest({
deploymentId: deployment.applicationDeploymentRecordId,
});
await this.db.updateDeploymentById(deployment.id, {
status: DeploymentStatus.Deleting,
applicationDeploymentRemovalRequestId:
result.applicationDeploymentRemovalRequestId,
applicationDeploymentRemovalRequestData:
result.applicationDeploymentRemovalRequestData,
});
return result !== undefined || result !== null;
}
return false;
}
async addDomain(
projectId: string,
data: { 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 = {
...data,
branch: currentProject.prodBranch,
project: currentProject,
};
const savedPrimaryDomain = await this.db.addDomain(primaryDomainDetails);
const domainArr = data.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,
data: DeepPartial<Domain>,
): Promise<boolean> {
const domain = await this.db.getDomain({
where: {
id: domainId,
},
});
if (domain === null) {
throw new Error(`Error finding domain with id ${domainId}`);
}
const newDomain = {
...data,
};
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 && data.branch === domain.branch) {
throw new Error('Remove all redirects to this domain before updating');
}
if (data.redirectToId) {
const redirectedDomain = await this.db.getDomain({
where: {
id: 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 this.db.updateDomainById(domainId, newDomain);
return updateResult;
}
async authenticateGitHub(
code: string,
user: User,
): Promise<{ token: string }> {
const {
authentication: { token },
} = await this.oauthApp.createToken({
code,
});
await this.db.updateUser(user, { gitHubToken: token });
return { token };
}
async unauthenticateGitHub(
user: User,
data: DeepPartial<User>,
): Promise<boolean> {
return this.db.updateUser(user, data);
}
}