import assert from 'node:assert' import path from 'node:path' import debug from 'debug' import { customAlphabet } from 'nanoid' import { lowercase, numbers } from 'nanoid-dictionary' import { DataSource, type DeepPartial, type FindManyOptions, type FindOneOptions, type FindOptionsWhere, IsNull, Not } from 'typeorm' import type { DatabaseConfig } from './config' import { Deployer } from './entity/Deployer' import { Deployment, DeploymentStatus } 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 { User } from './entity/User' import { UserOrganization } from './entity/UserOrganization' import type { DNSRecordAttributes } from './types' import { getEntities, loadAndSaveData } from './utils' 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 constructor({ dbPath }: DatabaseConfig) { this.dataSource = new DataSource({ type: 'better-sqlite3', database: dbPath, entities: [path.join(__dirname, '/entity/*')], synchronize: true, logging: false }) } async init(): Promise { await this.dataSource.initialize() log('database initialized') let organizations = await this.getOrganizations({}) // Load an organization if none exist if (!organizations.length) { const orgEntities = await getEntities( path.resolve(__dirname, ORGANIZATION_DATA_PATH) ) organizations = await loadAndSaveData(Organization, this.dataSource, [ orgEntities[0] ]) } // Hotfix for updating old DB data if (organizations[0].slug === 'snowball-tools-1') { const [orgEntity] = await getEntities( path.resolve(__dirname, ORGANIZATION_DATA_PATH) ) await this.updateOrganization(organizations[0].id, { slug: orgEntity.slug as string, name: orgEntity.name as string }) } } 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 updateOrganization( organizationId: string, data: DeepPartial ): Promise { const organizationRepository = this.dataSource.getRepository(Organization) const updateResult = await organizationRepository.update( { id: organizationId }, data ) assert(updateResult.affected) return updateResult.affected > 0 } 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 AND deployments.isCanonical = true' ) .leftJoinAndSelect('deployments.createdBy', 'user') .leftJoinAndSelect('deployments.deployer', 'deployer') .leftJoinAndSelect('project.owner', 'owner') .leftJoinAndSelect('project.deployers', 'deployers') .leftJoinAndSelect('project.organization', 'organization') .where('project.id = :projectId', { projectId }) .getOne() return project } async allProjectsWithoutDeployments(): Promise { const allProjects = await this.getProjects({ where: { auctionId: Not(IsNull()) }, relations: ['deployments'], withDeleted: true }) const projects = allProjects.filter((project) => { if (project.deletedAt !== null) return false return project.deployments.length === 0 }) return projects } 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 AND deployments.isCanonical = true' ) .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, createdBy: true, deployer: true }, where: { project: { id: projectId } }, order: { createdAt: 'DESC' } }) } async getNonCanonicalDeploymentsByProjectId( projectId: string ): Promise { return this.getDeployments({ relations: { project: true, createdBy: true, deployer: true }, where: { project: { id: projectId }, isCanonical: false }, 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 } 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 } 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 deleteDeploymentById(deploymentId: string): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment) const deployment = await deploymentRepository.findOneOrFail({ where: { id: deploymentId } }) const deleteResult = await deploymentRepository.softRemove(deployment) return Boolean(deleteResult) } 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 }) return projectRepository.save(newProject) } async saveProject(project: Project): Promise { const projectRepository = this.dataSource.getRepository(Project) return projectRepository.save(project) } 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 } 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 } async getOldestDomainByProjectId(projectId: string): Promise { const domainRepository = this.dataSource.getRepository(Domain) const domain = await domainRepository.findOne({ where: { project: { id: projectId } }, order: { createdAt: 'ASC' } }) return domain } async getLatestDNSRecordByProjectId( projectId: string ): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment) const deployment = await deploymentRepository.findOne({ where: { project: { id: projectId }, status: DeploymentStatus.Ready }, order: { createdAt: 'DESC' } }) if (deployment === null) { throw new Error(`No deployment found for project ${projectId}`) } return deployment.dnsRecordData } async addDeployer(data: DeepPartial): Promise { const deployerRepository = this.dataSource.getRepository(Deployer) const newDomain = await deployerRepository.save(data) return newDomain } async getDeployers(): Promise { const deployerRepository = this.dataSource.getRepository(Deployer) const deployers = await deployerRepository.find() return deployers } async getDeployerByLRN(deployerLrn: string): Promise { const deployerRepository = this.dataSource.getRepository(Deployer) const deployer = await deployerRepository.findOne({ where: { deployerLrn } }) return deployer } }