import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } 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, MiscConfig } from './config'; import { User } from './entity/User'; import { Organization } from './entity/Organization'; import { Project } from './entity/Project'; import { Deployment } from './entity/Deployment'; import { ProjectMember } from './entity/ProjectMember'; import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { Domain } from './entity/Domain'; import { getEntities, loadAndSaveData } from './utils'; import { UserOrganization } from './entity/UserOrganization'; const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json'; const log = debug('snowball:database'); const nanoid = customAlphabet(lowercase + numbers, 8); // TODO: Fix order of methods export class Database { private dataSource: DataSource; private projectDomain: string; constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) { this.dataSource = new DataSource({ type: 'better-sqlite3', database: dbPath, entities: [path.join(__dirname, '/entity/*')], synchronize: true, logging: false }); this.projectDomain = projectDomain; } async init (): Promise { await this.dataSource.initialize(); log('database initialized'); const organizations = await this.getOrganizations({}); // Load an organization if none exist if (!organizations.length) { const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH)); await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]); } } async getUser (options: FindOneOptions): Promise { const userRepository = this.dataSource.getRepository(User); const user = await userRepository.findOne(options); return user; } async addUser (data: DeepPartial): Promise { const userRepository = this.dataSource.getRepository(User); const user = await userRepository.save(data); return user; } async updateUser (user: User, data: DeepPartial): Promise { const userRepository = this.dataSource.getRepository(User); const updateResult = await userRepository.update({ id: user.id }, data); assert(updateResult.affected); return updateResult.affected > 0; } async getOrganizations ( options: FindManyOptions ): Promise { const organizationRepository = this.dataSource.getRepository(Organization); const organizations = await organizationRepository.find(options); return organizations; } async getOrganization ( options: FindOneOptions ): Promise { const organizationRepository = this.dataSource.getRepository(Organization); const organization = await organizationRepository.findOne(options); return organization; } async getOrganizationsByUserId (userId: string): Promise { const organizationRepository = this.dataSource.getRepository(Organization); const userOrgs = await organizationRepository.find({ where: { userOrganizations: { member: { id: userId } } } }); return userOrgs; } async addUserOrganization (data: DeepPartial): Promise { const userOrganizationRepository = this.dataSource.getRepository(UserOrganization); const newUserOrganization = await userOrganizationRepository.save(data); return newUserOrganization; } async getProjects (options: FindManyOptions): Promise { const projectRepository = this.dataSource.getRepository(Project); const projects = await projectRepository.find(options); return projects; } async getProjectById (projectId: string): Promise { const projectRepository = this.dataSource.getRepository(Project); const project = await projectRepository .createQueryBuilder('project') .leftJoinAndSelect( 'project.deployments', 'deployments', 'deployments.isCurrent = true' ) .leftJoinAndSelect('deployments.createdBy', 'user') .leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('project.owner', 'owner') .leftJoinAndSelect('project.organization', 'organization') .where('project.id = :projectId', { projectId }) .getOne(); return project; } async getProjectsInOrganization ( userId: string, organizationSlug: string ): Promise { const projectRepository = this.dataSource.getRepository(Project); const projects = await projectRepository .createQueryBuilder('project') .leftJoinAndSelect( 'project.deployments', 'deployments', 'deployments.isCurrent = true' ) .leftJoinAndSelect('deployments.domain', 'domain') .leftJoin('project.projectMembers', 'projectMembers') .leftJoin('project.organization', 'organization') .where( '(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', { userId, organizationSlug } ) .getMany(); return projects; } /** * Get deployments with specified filter */ async getDeployments ( options: FindManyOptions ): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const deployments = await deploymentRepository.find(options); return deployments; } async getDeploymentsByProjectId (projectId: string): Promise { return this.getDeployments({ relations: { project: true, domain: true, createdBy: true }, where: { project: { id: projectId } }, order: { createdAt: 'DESC' } }); } async getDeployment ( options: FindOneOptions ): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const deployment = await deploymentRepository.findOne(options); return deployment; } async getDomains (options: FindManyOptions): Promise { const domainRepository = this.dataSource.getRepository(Domain); const domains = await domainRepository.find(options); return domains; } async addDeployment (data: DeepPartial): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const id = nanoid(); const updatedData = { ...data, id }; const deployment = await deploymentRepository.save(updatedData); return deployment; } async getProjectMembersByProjectId ( projectId: string ): Promise { const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const projectMembers = await projectMemberRepository.find({ relations: { project: true, member: true }, where: { project: { id: projectId } } }); return projectMembers; } async getEnvironmentVariablesByProjectId ( projectId: string, filter?: FindOptionsWhere ): Promise { const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); const environmentVariables = await environmentVariableRepository.find({ where: { project: { id: projectId }, ...filter } }); return environmentVariables; } async removeProjectMemberById (projectMemberId: string): Promise { const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const deleteResult = await projectMemberRepository.delete({ id: projectMemberId }); if (deleteResult.affected) { return deleteResult.affected > 0; } else { return false; } } async updateProjectMemberById ( projectMemberId: string, data: DeepPartial ): Promise { const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const updateResult = await projectMemberRepository.update( { id: projectMemberId }, data ); return Boolean(updateResult.affected); } async addProjectMember ( data: DeepPartial ): Promise { const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const newProjectMember = await projectMemberRepository.save(data); return newProjectMember; } async addEnvironmentVariables ( data: DeepPartial[] ): Promise { const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); const savedEnvironmentVariables = await environmentVariableRepository.save(data); return savedEnvironmentVariables; } async updateEnvironmentVariable ( environmentVariableId: string, data: DeepPartial ): Promise { const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); const updateResult = await environmentVariableRepository.update( { id: environmentVariableId }, data ); return Boolean(updateResult.affected); } async deleteEnvironmentVariable ( environmentVariableId: string ): Promise { const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId }); if (deleteResult.affected) { return deleteResult.affected > 0; } else { return false; } } async getProjectMemberById (projectMemberId: string): Promise { const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const projectMemberWithProject = await projectMemberRepository.find({ relations: { project: { owner: true }, member: true }, where: { id: projectMemberId } }); if (projectMemberWithProject.length === 0) { throw new Error('Member does not exist'); } return projectMemberWithProject[0]; } async getProjectsBySearchText ( userId: string, searchText: string ): Promise { const projectRepository = this.dataSource.getRepository(Project); const projects = await projectRepository .createQueryBuilder('project') .leftJoinAndSelect('project.organization', 'organization') .leftJoin('project.projectMembers', 'projectMembers') .where( '(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', { userId, searchText: `%${searchText}%` } ) .getMany(); return projects; } async updateDeploymentById ( deploymentId: string, data: DeepPartial ): Promise { return this.updateDeployment({ id: deploymentId }, data); } async updateDeployment ( criteria: FindOptionsWhere, data: DeepPartial ): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const updateResult = await deploymentRepository.update(criteria, data); return Boolean(updateResult.affected); } async updateDeploymentsByProjectIds ( projectIds: string[], data: DeepPartial ): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const updateResult = await deploymentRepository .createQueryBuilder() .update(Deployment) .set(data) .where('projectId IN (:...projectIds)', { projectIds }) .execute(); return Boolean(updateResult.affected); } async addProject (user: User, organizationId: string, data: DeepPartial): Promise { const projectRepository = this.dataSource.getRepository(Project); // TODO: Check if organization exists const newProject = projectRepository.create(data); // TODO: Set default empty array for webhooks in TypeORM newProject.webhooks = []; // TODO: Set icon according to framework newProject.icon = ''; newProject.owner = user; newProject.organization = Object.assign(new Organization(), { id: organizationId }); newProject.subDomain = `${newProject.name}.${this.projectDomain}`; return projectRepository.save(newProject); } async updateProjectById ( projectId: string, data: DeepPartial ): Promise { const projectRepository = this.dataSource.getRepository(Project); const updateResult = await projectRepository.update( { id: projectId }, data ); return Boolean(updateResult.affected); } async deleteProjectById (projectId: string): Promise { const projectRepository = this.dataSource.getRepository(Project); const project = await projectRepository.findOneOrFail({ where: { id: projectId }, relations: { projectMembers: true } }); const deleteResult = await projectRepository.softRemove(project); return Boolean(deleteResult); } async deleteDomainById (domainId: string): Promise { const domainRepository = this.dataSource.getRepository(Domain); const deleteResult = await domainRepository.softDelete({ id: domainId }); if (deleteResult.affected) { return deleteResult.affected > 0; } else { return false; } } async addDomain (data: DeepPartial): Promise { const domainRepository = this.dataSource.getRepository(Domain); const newDomain = await domainRepository.save(data); return newDomain; } async getDomain (options: FindOneOptions): Promise { const domainRepository = this.dataSource.getRepository(Domain); const domain = await domainRepository.findOne(options); return domain; } async updateDomainById ( domainId: string, data: DeepPartial ): Promise { const domainRepository = this.dataSource.getRepository(Domain); const updateResult = await domainRepository.update({ id: domainId }, data); return Boolean(updateResult.affected); } async getDomainsByProjectId ( projectId: string, filter?: FindOptionsWhere ): Promise { const domainRepository = this.dataSource.getRepository(Domain); const domains = await domainRepository.find({ relations: { redirectTo: true }, where: { project: { id: projectId }, ...filter } }); return domains; } }