diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 263bc516..4c1c1a31 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -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 { + async getUser (options: FindOneOptions): Promise { 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): Promise { + 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): 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 createDeployement (data: DeepPartial): Promise { + const deploymentRepository = this.dataSource.getRepository(Deployment); + const deployment = await deploymentRepository.save(data); + + return deployment; + } + async getProjectMembersByProjectId (projectId: string): Promise { const projectMemberRepository = this.dataSource.getRepository(ProjectMember); @@ -201,60 +223,18 @@ export class Database { } } - async addProjectMember (projectId: string, data: { - email: string, - permissions: Permission[] - }): Promise { + async addProjectMember (data: DeepPartial): Promise { 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 { + async addEnvironmentVariables (data: DeepPartial[]): Promise { 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): Promise { @@ -363,40 +343,6 @@ export class Database { } } - async redeployToProdById (userId: string, deploymentId: string): Promise { - 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 { const projectRepository = this.dataSource.getRepository(Project); const project = await projectRepository.findOneOrFail({ @@ -416,16 +362,6 @@ export class Database { async deleteDomainById (domainId: string): Promise { 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) { diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index bec308a5..b58855ff 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -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}) => { + 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 }, context: any) => { - try { - await db.addProject(context.userId, projectDetails); - return true; - } catch (err) { - log(err); - return false; - } + addProject: async (_: any, { data }: { data: DeepPartial }, context: any) => { + return service.addProject(context.userId, data); }, updateProject: async (_: any, { projectId, projectDetails }: { projectId: string, projectDetails: DeepPartial }) => { - 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 }) => { diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index ecd05a0d..313834d1 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -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! diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index 8c3798c4..ed2272c4 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -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 { - return this.db.getUser(userId); + return this.db.getUser({ + where: { + id: userId + } + }); } async getOrganizationsByUserId (userId: number): Promise { @@ -57,4 +70,189 @@ export class Service { const dbDomains = await this.db.getDomainsByProjectId(projectId); return dbDomains; } + + async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise { + try { + return await this.db.updateProjectMemberById(projectMemberId, data); + } catch (err) { + log(err); + return false; + } + } + + async addProjectMember (projectId: string, + data: { + email: string, + permissions: Permission[] + }): Promise { + 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 { + 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): Promise { + try { + return await this.db.updateEnvironmentVariable(environmentVariableId, data); + } catch (err) { + log(err); + return false; + } + } + + async removeEnvironmentVariable (environmentVariableId: string): Promise { + try { + return await this.db.deleteEnvironmentVariable(environmentVariableId); + } catch (err) { + log(err); + return false; + } + } + + async updateDeploymentToProd (deploymentId: string): Promise { + try { + return await this.db.updateDeploymentById(deploymentId, { + environment: Environment.Production + }); + } catch (err) { + log(err); + return false; + } + } + + async addProject (userId: string, data: DeepPartial): Promise { + try { + await this.db.addProject(userId, data); + return true; + } catch (err) { + log(err); + return false; + } + } + + async updateProject (projectId: string, data: DeepPartial): Promise { + try { + return await this.db.updateProjectById(projectId, data); + } catch (err) { + log(err); + return false; + } + } + + async deleteProject (projectId: string): Promise { + try { + return await this.db.deleteProjectById(projectId); + } catch (err) { + log(err); + return false; + } + } + + async deleteDomain (domainId: string): Promise { + 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 { + 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; + } + } } diff --git a/packages/frontend/src/components/Sidebar.tsx b/packages/frontend/src/components/Sidebar.tsx index 5246d4eb..9e7abd86 100644 --- a/packages/frontend/src/components/Sidebar.tsx +++ b/packages/frontend/src/components/Sidebar.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { Link, NavLink } from 'react-router-dom'; +import { Organization } from 'gql-client'; import { Card, CardBody, Typography } from '@material-tailwind/react'; -const Sidebar = () => { +const Sidebar = ({ organization }: { organization: Organization }) => { return (
@@ -13,8 +14,12 @@ const Sidebar = () => {
- - Organization + +
^
+
+ {organization.name} + Organization +
diff --git a/packages/frontend/src/layouts/Dashboard.tsx b/packages/frontend/src/layouts/Dashboard.tsx index 9f187a9c..b637210d 100644 --- a/packages/frontend/src/layouts/Dashboard.tsx +++ b/packages/frontend/src/layouts/Dashboard.tsx @@ -1,19 +1,42 @@ -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Organization } from 'gql-client'; + import { Outlet } from 'react-router-dom'; import Sidebar from '../components/Sidebar'; +import { useGQLClient } from '../context/GQLClientContext'; + +// TODO: Implement organization switcher +// TODO: Projects get organization details through routes instead of context +const USER_ORGANIZATION_INDEX = 0; const Dashboard = () => { + const client = useGQLClient(); + const [organizations, setOrganizations] = useState([]); + + const fetchUserOrganizations = useCallback(async () => { + const { organizations } = await client.getOrganizations(); + setOrganizations(organizations); + }, []); + + useEffect(() => { + fetchUserOrganizations(); + }, []); + return (
-
- -
-
-
- -
-
+ {organizations.length > 0 && ( + <> +
+ +
+
+
+ +
+
+ + )}
); }; diff --git a/packages/frontend/src/layouts/ProjectSearch.tsx b/packages/frontend/src/layouts/ProjectSearch.tsx index f70c7652..32be1155 100644 --- a/packages/frontend/src/layouts/ProjectSearch.tsx +++ b/packages/frontend/src/layouts/ProjectSearch.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { Outlet, useNavigate } from 'react-router-dom'; +import { Outlet, useNavigate, useOutletContext } from 'react-router-dom'; +import { Organization } from 'gql-client'; import { IconButton, Typography } from '@material-tailwind/react'; @@ -8,7 +9,7 @@ import ProjectSearchBar from '../components/projects/ProjectSearchBar'; const ProjectSearch = () => { const navigate = useNavigate(); - + const organization = useOutletContext(); return (
@@ -33,7 +34,7 @@ const ProjectSearch = () => {
- +
); diff --git a/packages/frontend/src/pages/index.tsx b/packages/frontend/src/pages/index.tsx index f27e56a2..cb0524e6 100644 --- a/packages/frontend/src/pages/index.tsx +++ b/packages/frontend/src/pages/index.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useOutletContext } from 'react-router-dom'; +import { Organization } from 'gql-client'; import { Button, Typography, Chip } from '@material-tailwind/react'; @@ -8,16 +9,16 @@ import { useGQLClient } from '../context/GQLClientContext'; import { ProjectDetails } from '../types/project'; import { COMMIT_DETAILS } from '../constants'; -// TODO: Implement organization switcher -const USER_ORGANIZATION_ID = '1'; - const Projects = () => { const client = useGQLClient(); + const organization = useOutletContext(); + const [projects, setProjects] = useState([]); const fetchProjects = useCallback(async () => { - const { projectsInOrganization } = - await client.getProjectsInOrganization(USER_ORGANIZATION_ID); + const { projectsInOrganization } = await client.getProjectsInOrganization( + organization.id, + ); const updatedProjects = projectsInOrganization.map((project) => { return { diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index 4cc547ff..d4a24265 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -148,28 +148,28 @@ export class GQLClient { return data; } - async addEnvironmentVariables (projectId: string, environmentVariables: AddEnvironmentVariableInput[]): Promise { - const { data } = await this.client.mutate({ + async addEnvironmentVariables (projectId: string, data: AddEnvironmentVariableInput[]): Promise { + const result = await this.client.mutate({ mutation: addEnvironmentVariables, variables: { projectId, - environmentVariables + data } }); - return data; + return result.data; } - async updateEnvironmentVariable (environmentVariableId: string, environmentVariable: UpdateEnvironmentVariableInput): Promise { - const { data } = await this.client.mutate({ + async updateEnvironmentVariable (environmentVariableId: string, data: UpdateEnvironmentVariableInput): Promise { + const result = await this.client.mutate({ mutation: updateEnvironmentVariable, variables: { environmentVariableId, - environmentVariable + data } }); - return data; + return result.data; } async removeEnvironmentVariable (environmentVariableId: string): Promise { diff --git a/packages/gql-client/src/mutations.ts b/packages/gql-client/src/mutations.ts index 646d33e0..3506c313 100644 --- a/packages/gql-client/src/mutations.ts +++ b/packages/gql-client/src/mutations.ts @@ -19,14 +19,14 @@ mutation ($projectId: String!, $data: AddProjectMemberInput) { `; export const addEnvironmentVariables = gql` -mutation ($projectId: String!, $environmentVariables: [AddEnvironmentVariableInput!]) { - addEnvironmentVariables(projectId: $projectId, environmentVariables: $environmentVariables) +mutation ($projectId: String!, $data: [AddEnvironmentVariableInput!]) { + addEnvironmentVariables(projectId: $projectId, data: $data) } `; export const updateEnvironmentVariable = gql` -mutation ($environmentVariableId: String!, $environmentVariable: UpdateEnvironmentVariableInput!) { - updateEnvironmentVariable(environmentVariableId: $environmentVariableId, environmentVariable: $environmentVariable) +mutation ($environmentVariableId: String!, $data: UpdateEnvironmentVariableInput!) { + updateEnvironmentVariable(environmentVariableId: $environmentVariableId, data: $data) } `;