Refactor mutation GQL methods to service class and display organization name (#53)

* Refactor mutation methods to service class

* Refactor database methods to service class

* Display organization name in sidebar

* Handle review comments

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
2024-02-05 16:21:55 +05:30
committed by GitHub
co-authored by neeraj
parent ac7064afa5
commit da92ddfba3
10 changed files with 320 additions and 218 deletions
+39 -103
View File
@@ -1,24 +1,20 @@
import { DataSource, DeepPartial } from 'typeorm';
import { DataSource, DeepPartial, FindManyOptions, FindOneOptions } from 'typeorm';
import path from 'path';
import debug from 'debug';
import assert from 'assert';
import { customAlphabet } from 'nanoid';
import { lowercase, numbers } from 'nanoid-dictionary';
import { DatabaseConfig } from './config';
import { User } from './entity/User';
import { Organization } from './entity/Organization';
import { Project } from './entity/Project';
import { Deployment, Environment } from './entity/Deployment';
import { Permission, ProjectMember } from './entity/ProjectMember';
import { Deployment } from './entity/Deployment';
import { ProjectMember } from './entity/ProjectMember';
import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { Domain } from './entity/Domain';
import { PROJECT_DOMAIN } from './constants';
const log = debug('snowball:database');
const nanoid = customAlphabet(lowercase + numbers, 8);
// TODO: Fix order of methods
export class Database {
private dataSource: DataSource;
@@ -38,11 +34,16 @@ export class Database {
log('database initialized');
}
async getUser (userId: number): Promise<User | null> {
async getUser (options: FindOneOptions<User>): Promise<User | null> {
const userRepository = this.dataSource.getRepository(User);
const user = await userRepository.findOneBy({
id: userId
});
const user = await userRepository.findOne(options);
return user;
}
async createUser (data: DeepPartial<User>): Promise<User> {
const userRepository = this.dataSource.getRepository(User);
const user = await userRepository.save(data);
return user;
}
@@ -146,6 +147,27 @@ export class Database {
return deployments;
}
async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.findOne(options);
return deployment;
}
async getDomains (options: FindManyOptions<Domain>): Promise<Domain[]> {
const domainRepository = this.dataSource.getRepository(Domain);
const domains = await domainRepository.find(options);
return domains;
}
async createDeployement (data: DeepPartial<Deployment>): Promise<Deployment> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.save(data);
return deployment;
}
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
@@ -201,60 +223,18 @@ export class Database {
}
}
async addProjectMember (projectId: string, data: {
email: string,
permissions: Permission[]
}): Promise<boolean> {
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
const userRepository = this.dataSource.getRepository(User);
const newProjectMember = await projectMemberRepository.save(data);
let user = await userRepository.findOneBy({
email: data.email
});
if (!user) {
user = await userRepository.save({
email: data.email,
isVerified: false
});
}
const newProjectMember = await projectMemberRepository.save({
project: {
id: projectId
},
permissions: data.permissions,
isPending: true,
member: {
id: user.id
}
});
return Boolean(newProjectMember);
return newProjectMember;
}
async addEnvironmentVariablesByProjectId (projectId: string, environmentVariables: {
environments: string[];
key: string;
value: string;
}[]): Promise<boolean> {
async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> {
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
const savedEnvironmentVariables = await environmentVariableRepository.save(data);
const formattedEnvironmentVariables = environmentVariables.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 environmentVariableRepository.save(formattedEnvironmentVariables);
return savedEnvironmentVariables.length > 0;
return savedEnvironmentVariables;
}
async updateEnvironmentVariable (environmentVariableId: string, update: DeepPartial<EnvironmentVariable>): Promise<boolean> {
@@ -363,40 +343,6 @@ export class Database {
}
}
async redeployToProdById (userId: string, deploymentId: string): Promise<Deployment> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.findOne({
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)
});
}
await deploymentRepository.update({ id: deploymentId }, { domain: null, isCurrent: false });
updatedDeployment.id = nanoid();
updatedDeployment.url = `${updatedDeployment.id}-${updatedDeployment.project.subDomain}`;
return deploymentRepository.save(updatedDeployment);
}
async deleteProjectById (projectId: string): Promise<boolean> {
const projectRepository = this.dataSource.getRepository(Project);
const project = await projectRepository.findOneOrFail({
@@ -416,16 +362,6 @@ export class Database {
async deleteDomainById (domainId: string): Promise<boolean> {
const domainRepository = this.dataSource.getRepository(Domain);
const domainsRedirectedFrom = await domainRepository.find({
where: {
redirectToId: Number(domainId)
}
});
if (domainsRedirectedFrom.length > 0) {
throw new Error('Cannot delete domain since it has redirects from other domains');
}
const deleteResult = await domainRepository.softDelete({ id: Number(domainId) });
if (deleteResult.affected) {
+15 -77
View File
@@ -7,10 +7,10 @@ import { OAuthApp } from '@octokit/oauth-app';
import { Service } from './service';
import { Database } from './database';
import { isUserOwner } from './utils';
import { Environment } from './entity/Deployment';
import { Permission } from './entity/ProjectMember';
import { Domain } from './entity/Domain';
import { Project } from './entity/Project';
import { EnvironmentVariable } from './entity/EnvironmentVariable';
const log = debug('snowball:database');
@@ -86,12 +86,7 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
permissions: Permission[]
}
}) => {
try {
return await db.updateProjectMemberById(projectMemberId, data);
} catch (err) {
log(err);
return false;
}
return service.updateProjectMember(projectMemberId, data);
},
addProjectMember: async (_: any, { projectId, data }: {
@@ -101,100 +96,43 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
permissions: Permission[]
}
}) => {
try {
// TODO: Send invitation
return await db.addProjectMember(projectId, data);
} catch (err) {
log(err);
return false;
}
return service.addProjectMember(projectId, data);
},
addEnvironmentVariables: async (_: any, { projectId, environmentVariables }: { projectId: string, environmentVariables: { environments: string[], key: string, value: string}[] }) => {
try {
return await db.addEnvironmentVariablesByProjectId(projectId, environmentVariables);
} 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);
},
updateEnvironmentVariable: async (_: any, { environmentVariableId, environmentVariable }: { environmentVariableId: string, environmentVariable : {
key: string
value: string
}}) => {
try {
return await db.updateEnvironmentVariable(environmentVariableId, environmentVariable);
} catch (err) {
log(err);
return false;
}
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => {
return service.updateEnvironmentVariable(environmentVariableId, data);
},
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
try {
return await db.deleteEnvironmentVariable(environmentVariableId);
} catch (err) {
log(err);
return false;
}
return service.removeEnvironmentVariable(environmentVariableId);
},
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }) => {
try {
return await db.updateDeploymentById(deploymentId, {
environment: Environment.Production
});
} catch (err) {
log(err);
return false;
}
return service.updateDeploymentToProd(deploymentId);
},
addProject: async (_: any, { projectDetails }: { projectDetails: DeepPartial<Project> }, context: any) => {
try {
await db.addProject(context.userId, projectDetails);
return true;
} catch (err) {
log(err);
return false;
}
addProject: async (_: any, { data }: { data: DeepPartial<Project> }, context: any) => {
return service.addProject(context.userId, data);
},
updateProject: async (_: any, { projectId, projectDetails }: { projectId: string, projectDetails: DeepPartial<Project> }) => {
try {
return await db.updateProjectById(projectId, projectDetails);
} catch (err) {
log(err);
return false;
}
return service.updateProject(projectId, projectDetails);
},
redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
try {
return await db.redeployToProdById(context.userId, deploymentId);
} catch (err) {
log(err);
return false;
}
return service.redeployToProd(context.userId, deploymentId);
},
deleteProject: async (_: any, { projectId }: { projectId: string }) => {
try {
return await db.deleteProjectById(projectId);
} catch (err) {
log(err);
return false;
}
return service.deleteProject(projectId);
},
deleteDomain: async (_: any, { domainId }: { domainId: string }) => {
try {
return await db.deleteDomainById(domainId);
} catch (err) {
log(err);
return false;
}
return service.deleteDomain(domainId);
},
rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => {
+2 -2
View File
@@ -184,8 +184,8 @@ type Mutation {
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
removeProjectMember(projectMemberId: String!): Boolean!
addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean!
updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean!
addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean!
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean!
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateDeploymentToProd(deploymentId: String!): Boolean!
addProject(projectDetails: AddProjectInput): Boolean!
+201 -3
View File
@@ -1,11 +1,20 @@
import debug from 'debug';
import { customAlphabet } from 'nanoid';
import { lowercase, numbers } from 'nanoid-dictionary';
import { Database } from './database';
import { Deployment } from './entity/Deployment';
import { Deployment, 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 { ProjectMember } from './entity/ProjectMember';
import { Permission, ProjectMember } from './entity/ProjectMember';
import { User } from './entity/User';
import { DeepPartial } from 'typeorm';
const log = debug('snowball:service');
const nanoid = customAlphabet(lowercase + numbers, 8);
export class Service {
private db: Database;
@@ -15,7 +24,11 @@ export class Service {
}
async getUser (userId: number): Promise<User | null> {
return this.db.getUser(userId);
return this.db.getUser({
where: {
id: userId
}
});
}
async getOrganizationsByUserId (userId: number): Promise<Organization[]> {
@@ -57,4 +70,189 @@ export class Service {
const dbDomains = await this.db.getDomainsByProjectId(projectId);
return dbDomains;
}
async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise<boolean> {
try {
return await this.db.updateProjectMemberById(projectMemberId, data);
} catch (err) {
log(err);
return false;
}
}
async addProjectMember (projectId: string,
data: {
email: string,
permissions: Permission[]
}): Promise<boolean> {
try {
// TODO: Send invitation
let user = await this.db.getUser({
where: {
email: data.email
}
});
if (!user) {
user = await this.db.createUser({
email: data.email
});
}
const newProjectMember = await this.db.addProjectMember({
project: {
id: projectId
},
permissions: data.permissions,
isPending: true,
member: {
id: user.id
}
});
return Boolean(newProjectMember);
} catch (err) {
log(err);
return false;
}
}
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<boolean> {
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
})
});
});
}).flat();
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables);
return savedEnvironmentVariables.length > 0;
} catch (err) {
log(err);
return false;
}
}
async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial<EnvironmentVariable>): Promise<boolean> {
try {
return await this.db.updateEnvironmentVariable(environmentVariableId, data);
} catch (err) {
log(err);
return false;
}
}
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
try {
return await this.db.deleteEnvironmentVariable(environmentVariableId);
} catch (err) {
log(err);
return false;
}
}
async updateDeploymentToProd (deploymentId: string): Promise<boolean> {
try {
return await this.db.updateDeploymentById(deploymentId, {
environment: Environment.Production
});
} catch (err) {
log(err);
return false;
}
}
async addProject (userId: string, data: DeepPartial<Project>): Promise<boolean> {
try {
await this.db.addProject(userId, data);
return true;
} catch (err) {
log(err);
return false;
}
}
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
try {
return await this.db.updateProjectById(projectId, data);
} catch (err) {
log(err);
return false;
}
}
async deleteProject (projectId: string): Promise<boolean> {
try {
return await this.db.deleteProjectById(projectId);
} catch (err) {
log(err);
return false;
}
}
async deleteDomain (domainId: string): Promise<boolean> {
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');
}
return await this.db.deleteDomainById(domainId);
} catch (err) {
log(err);
return false;
}
}
async redeployToProd (userId: string, deploymentId: string): Promise<Deployment| boolean> {
try {
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.createDeployement(updatedDeployment);
return oldDeployment && Boolean(newDeployement);
} catch (err) {
log(err);
return false;
}
}
}