diff --git a/build-webapp.sh b/build-webapp.sh new file mode 100755 index 00000000..e3e8db99 --- /dev/null +++ b/build-webapp.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +PKG_DIR="./packages/frontend" +OUTPUT_DIR="${PKG_DIR}/build" +DEST_DIR=${1:-/data} + +if [[ -d "$DEST_DIR" ]]; then + echo "${DEST_DIR} already exists." 1>&2 + exit 1 +fi + +cat > $PKG_DIR/.env <&2 + exit 1 +fi + +mv "$OUTPUT_DIR" "$DEST_DIR" diff --git a/packages/backend/.eslintrc.json b/packages/backend/.eslintrc.json index da3b01c6..c7012cab 100644 --- a/packages/backend/.eslintrc.json +++ b/packages/backend/.eslintrc.json @@ -25,6 +25,9 @@ "allowArgumentsExplicitlyTypedAsAny": true } ], - "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }] + "@typescript-eslint/no-unused-vars": [ + "error", + { "ignoreRestSiblings": true } + ] } } diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 1a98a657..55c4f773 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -13,7 +13,7 @@ export interface GitHubConfig { oAuth: { clientId: string; clientSecret: string; - } + }; } export interface RegistryConfig { @@ -27,7 +27,7 @@ export interface RegistryConfig { amount: string; denom: string; gas: string; - } + }; } export interface Config { diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts index 7803a7b1..916d2004 100644 --- a/packages/backend/src/constants.ts +++ b/packages/backend/src/constants.ts @@ -1,8 +1,14 @@ -export const DEFAULT_CONFIG_FILE_PATH = 'environments/local.toml'; +import process from 'process'; + +export const DEFAULT_CONFIG_FILE_PATH = + process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml'; export const DEFAULT_GQL_PATH = '/graphql'; // Note: temporary hardcoded user, later to be derived from auth token -export const USER_ID = '59f4355d-9549-4aac-9b54-eeefceeabef0'; +export const USER_ID = + process.env.SNOWBALL_BACKEND_USER_ID || + '59f4355d-9549-4aac-9b54-eeefceeabef0'; -export const PROJECT_DOMAIN = 'snowball.xyz'; +export const PROJECT_DOMAIN = + process.env.SNOWBALL_BACKEND_PROJECT_DOMAIN || 'snowball.xyz'; diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index a45c354d..324051a7 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -1,4 +1,10 @@ -import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm'; +import { + DataSource, + DeepPartial, + FindManyOptions, + FindOneOptions, + FindOptionsWhere +} from 'typeorm'; import path from 'path'; import debug from 'debug'; import assert from 'assert'; @@ -14,6 +20,12 @@ import { ProjectMember } from './entity/ProjectMember'; import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { Domain } from './entity/Domain'; import { PROJECT_DOMAIN } from './constants'; +import { getEntities, loadAndSaveData } from './utils'; +import { UserOrganization } from './entity/UserOrganization'; + +const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json'; +const USER_DATA_PATH = '../test/fixtures/users.json'; +const USER_ORGANIZATION_DATA_PATH = '../test/fixtures/user-organizations.json'; const log = debug('snowball:database'); @@ -36,6 +48,40 @@ export class Database { async init (): Promise { await this.dataSource.initialize(); log('database initialized'); + + const organizations = await this.getOrganizations({}); + + if (!organizations.length) { + const orgEntities = await getEntities( + path.resolve(__dirname, ORGANIZATION_DATA_PATH) + ); + const savedOrgs = await loadAndSaveData(Organization, this.dataSource, [ + orgEntities[0] + ]); + + // TODO: Remove user once authenticated + const userEntities = await getEntities( + path.resolve(__dirname, USER_DATA_PATH) + ); + const savedUsers = await loadAndSaveData(User, this.dataSource, [ + userEntities[0] + ]); + + const userOrganizationRelations = { + member: savedUsers, + organization: savedOrgs + }; + + const userOrgEntities = await getEntities( + path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH) + ); + await loadAndSaveData( + UserOrganization, + this.dataSource, + [userOrgEntities[0]], + userOrganizationRelations + ); + } } async getUser (options: FindOneOptions): Promise { @@ -60,7 +106,18 @@ export class Database { return updateResult.affected > 0; } - async getOrganization (options: FindOneOptions): Promise { + 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); @@ -95,7 +152,11 @@ export class Database { const project = await projectRepository .createQueryBuilder('project') - .leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true') + .leftJoinAndSelect( + 'project.deployments', + 'deployments', + 'deployments.isCurrent = true' + ) .leftJoinAndSelect('deployments.createdBy', 'user') .leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('project.owner', 'owner') @@ -108,19 +169,29 @@ export class Database { return project; } - async getProjectsInOrganization (userId: string, organizationSlug: string): Promise { + 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( + '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 - }) + .where( + '(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', + { + userId, + organizationSlug + } + ) .getMany(); return projects; @@ -129,7 +200,9 @@ export class Database { /** * Get deployments with specified filter */ - async getDeployments (options: FindManyOptions): Promise { + async getDeployments ( + options: FindManyOptions + ): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const deployments = await deploymentRepository.find(options); @@ -154,7 +227,9 @@ export class Database { }); } - async getDeployment (options: FindOneOptions): Promise { + async getDeployment ( + options: FindOneOptions + ): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const deployment = await deploymentRepository.findOne(options); @@ -182,8 +257,11 @@ export class Database { return deployment; } - async getProjectMembersByProjectId (projectId: string): Promise { - const projectMemberRepository = this.dataSource.getRepository(ProjectMember); + async getProjectMembersByProjectId ( + projectId: string + ): Promise { + const projectMemberRepository = + this.dataSource.getRepository(ProjectMember); const projectMembers = await projectMemberRepository.find({ relations: { @@ -200,8 +278,12 @@ export class Database { return projectMembers; } - async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere): Promise { - const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); + async getEnvironmentVariablesByProjectId ( + projectId: string, + filter?: FindOptionsWhere + ): Promise { + const environmentVariableRepository = + this.dataSource.getRepository(EnvironmentVariable); const environmentVariables = await environmentVariableRepository.find({ where: { @@ -216,9 +298,12 @@ export class Database { } async removeProjectMemberById (projectMemberId: string): Promise { - const projectMemberRepository = this.dataSource.getRepository(ProjectMember); + const projectMemberRepository = + this.dataSource.getRepository(ProjectMember); - const deleteResult = await projectMemberRepository.delete({ id: projectMemberId }); + const deleteResult = await projectMemberRepository.delete({ + id: projectMemberId + }); if (deleteResult.affected) { return deleteResult.affected > 0; @@ -227,37 +312,63 @@ export class Database { } } - async updateProjectMemberById (projectMemberId: string, data: DeepPartial): Promise { - const projectMemberRepository = this.dataSource.getRepository(ProjectMember); - const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data); + 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); + 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); + 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); + 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 }); + 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; @@ -267,7 +378,8 @@ export class Database { } async getProjectMemberById (projectMemberId: string): Promise { - const projectMemberRepository = this.dataSource.getRepository(ProjectMember); + const projectMemberRepository = + this.dataSource.getRepository(ProjectMember); const projectMemberWithProject = await projectMemberRepository.find({ relations: { @@ -279,8 +391,7 @@ export class Database { where: { id: projectMemberId } - } - ); + }); if (projectMemberWithProject.length === 0) { throw new Error('Member does not exist'); @@ -289,34 +400,49 @@ export class Database { return projectMemberWithProject[0]; } - async getProjectsBySearchText (userId: string, searchText: string): Promise { + 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}%` - }) + .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 { + async updateDeploymentById ( + deploymentId: string, + data: DeepPartial + ): Promise { return this.updateDeployment({ id: deploymentId }, data); } - async updateDeployment (criteria: FindOptionsWhere, data: DeepPartial): Promise { + 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 { + async updateDeploymentsByProjectIds ( + projectIds: string[], + data: DeepPartial + ): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const updateResult = await deploymentRepository @@ -329,7 +455,11 @@ export class Database { return Boolean(updateResult.affected); } - async addProject (userId: string, organizationId: string, data: DeepPartial): Promise { + async addProject ( + userId: string, + organizationId: string, + data: DeepPartial + ): Promise { const projectRepository = this.dataSource.getRepository(Project); // TODO: Check if organization exists @@ -352,9 +482,15 @@ export class Database { return projectRepository.save(newProject); } - async updateProjectById (projectId: string, data: DeepPartial): Promise { + async updateProjectById ( + projectId: string, + data: DeepPartial + ): Promise { const projectRepository = this.dataSource.getRepository(Project); - const updateResult = await projectRepository.update({ id: projectId }, data); + const updateResult = await projectRepository.update( + { id: projectId }, + data + ); return Boolean(updateResult.affected); } @@ -401,14 +537,20 @@ export class Database { return domain; } - async updateDomainById (domainId: string, data: DeepPartial): Promise { + 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 { + async getDomainsByProjectId ( + projectId: string, + filter?: FindOptionsWhere + ): Promise { const domainRepository = this.dataSource.getRepository(Domain); const domains = await domainRepository.find({ diff --git a/packages/backend/src/entity/Deployment.ts b/packages/backend/src/entity/Deployment.ts index 62d3f38f..b8397a31 100644 --- a/packages/backend/src/entity/Deployment.ts +++ b/packages/backend/src/entity/Deployment.ts @@ -26,18 +26,27 @@ export enum DeploymentStatus { Error = 'Error', } +export interface ApplicationDeploymentRequest { + type: string; + version: string; + name: string; + application: string; + config: string; + meta: string; +} + export interface ApplicationRecord { type: string; - version:string - name: string - description?: string - homepage?: string - license?: string - author?: string - repository?: string[], - app_version?: string - repository_ref: string - app_type: string + version: string; + name: string; + description?: string; + homepage?: string; + license?: string; + author?: string; + repository?: string[]; + app_version?: string; + repository_ref: string; + app_type: string; } @Entity() @@ -78,6 +87,12 @@ export class Deployment { @Column('simple-json') applicationRecordData!: ApplicationRecord; + @Column('varchar') + applicationDeploymentRequestId!: string; + + @Column('simple-json') + applicationDeploymentRequestData!: ApplicationDeploymentRequest; + @Column('varchar', { nullable: true }) applicationDeploymentRecordId!: string | null; diff --git a/packages/backend/src/entity/Domain.ts b/packages/backend/src/entity/Domain.ts index 29f189ff..c662daff 100644 --- a/packages/backend/src/entity/Domain.ts +++ b/packages/backend/src/entity/Domain.ts @@ -39,7 +39,7 @@ export class Domain { @ManyToOne(() => Domain) @JoinColumn({ name: 'redirectToId' }) - // eslint-disable-next-line no-use-before-define + // eslint-disable-next-line no-use-before-define redirectTo!: Domain | null; @Column({ diff --git a/packages/backend/src/entity/Organization.ts b/packages/backend/src/entity/Organization.ts index 15ff1011..3b15522d 100644 --- a/packages/backend/src/entity/Organization.ts +++ b/packages/backend/src/entity/Organization.ts @@ -27,8 +27,12 @@ export class Organization { @UpdateDateColumn() updatedAt!: Date; - @OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, { - cascade: ['soft-remove'] - }) + @OneToMany( + () => UserOrganization, + (userOrganization) => userOrganization.organization, + { + cascade: ['soft-remove'] + } + ) userOrganizations!: UserOrganization[]; } diff --git a/packages/backend/src/entity/Project.ts b/packages/backend/src/entity/Project.ts index 86fa7b3d..b46c219a 100644 --- a/packages/backend/src/entity/Project.ts +++ b/packages/backend/src/entity/Project.ts @@ -15,15 +15,6 @@ import { Organization } from './Organization'; import { ProjectMember } from './ProjectMember'; import { Deployment } from './Deployment'; -export interface ApplicationDeploymentRequest { - type: string - version: string - name: string - application: string - config: string, - meta: string -} - @Entity() export class Project { @PrimaryGeneratedColumn('uuid') @@ -52,12 +43,6 @@ export class Project { @Column('varchar', { length: 255, default: 'main' }) prodBranch!: string; - @Column('varchar', { nullable: true }) - applicationDeploymentRequestId!: string | null; - - @Column('simple-json', { nullable: true }) - applicationDeploymentRequestData!: ApplicationDeploymentRequest | null; - @Column('text', { default: '' }) description!: string; @@ -91,7 +76,7 @@ export class Project { @OneToMany(() => Deployment, (deployment) => deployment.project) deployments!: Deployment[]; - @OneToMany(() => ProjectMember, projectMember => projectMember.project, { + @OneToMany(() => ProjectMember, (projectMember) => projectMember.project, { cascade: ['soft-remove'] }) projectMembers!: ProjectMember[]; diff --git a/packages/backend/src/entity/ProjectMember.ts b/packages/backend/src/entity/ProjectMember.ts index d4e77edc..545a64db 100644 --- a/packages/backend/src/entity/ProjectMember.ts +++ b/packages/backend/src/entity/ProjectMember.ts @@ -15,7 +15,7 @@ import { User } from './User'; export enum Permission { View = 'View', - Edit = 'Edit' + Edit = 'Edit', } @Entity() diff --git a/packages/backend/src/entity/User.ts b/packages/backend/src/entity/User.ts index d280edc7..8f8a8c54 100644 --- a/packages/backend/src/entity/User.ts +++ b/packages/backend/src/entity/User.ts @@ -34,13 +34,17 @@ export class User { @CreateDateColumn() updatedAt!: Date; - @OneToMany(() => ProjectMember, projectMember => projectMember.project, { + @OneToMany(() => ProjectMember, (projectMember) => projectMember.project, { cascade: ['soft-remove'] }) projectMembers!: ProjectMember[]; - @OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, { - cascade: ['soft-remove'] - }) + @OneToMany( + () => UserOrganization, + (UserOrganization) => UserOrganization.member, + { + cascade: ['soft-remove'] + } + ) userOrganizations!: UserOrganization[]; } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 7a871a28..6fb7e812 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -19,7 +19,9 @@ const OAUTH_CLIENT_TYPE = 'oauth-app'; export const main = async (): Promise => { // TODO: get config path using cli - const { server, database, gitHub, registryConfig } = await getConfig(DEFAULT_CONFIG_FILE_PATH); + const { server, database, gitHub, registryConfig } = await getConfig( + DEFAULT_CONFIG_FILE_PATH + ); const app = new OAuthApp({ clientType: OAUTH_CLIENT_TYPE, @@ -31,9 +33,16 @@ export const main = async (): Promise => { await db.init(); const registry = new Registry(registryConfig); - const service = new Service({ gitHubConfig: gitHub, registryConfig }, db, app, registry); + const service = new Service( + { gitHubConfig: gitHub, registryConfig }, + db, + app, + registry + ); - const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString(); + const typeDefs = fs + .readFileSync(path.join(__dirname, 'schema.gql')) + .toString(); const resolvers = await createResolvers(service); await createAndStartServer(server, typeDefs, resolvers, service); diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 108318bb..0c4b3bf2 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -6,8 +6,11 @@ import { DateTime } from 'luxon'; import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk'; import { RegistryConfig } from './config'; -import { ApplicationDeploymentRequest } from './entity/Project'; -import { ApplicationRecord, Deployment } from './entity/Deployment'; +import { + ApplicationRecord, + Deployment, + ApplicationDeploymentRequest +} from './entity/Deployment'; import { AppDeploymentRecord, PackageJSON } from './types'; const log = debug('snowball:registry'); @@ -21,35 +24,54 @@ export class Registry { private registry: LaconicRegistry; private registryConfig: RegistryConfig; - constructor (registryConfig : RegistryConfig) { + constructor (registryConfig: RegistryConfig) { this.registryConfig = registryConfig; - this.registry = new LaconicRegistry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId); + this.registry = new LaconicRegistry( + registryConfig.gqlEndpoint, + registryConfig.restEndpoint, + registryConfig.chainId + ); } async createApplicationRecord ({ + appName, packageJSON, commitHash, appType, repoUrl }: { - packageJSON: PackageJSON - commitHash: string, - appType: string, - repoUrl: string - }): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> { - assert(packageJSON.name, "name field doesn't exist in package.json"); + appName: string; + packageJSON: PackageJSON; + commitHash: string; + appType: string; + repoUrl: string; + }): Promise<{ + applicationRecordId: string; + applicationRecordData: ApplicationRecord; + }> { // Use laconic-sdk to publish record // Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh // Fetch previous records - const records = await this.registry.queryRecords({ - type: APP_RECORD_TYPE, - name: packageJSON.name - }, true); + const records = await this.registry.queryRecords( + { + type: APP_RECORD_TYPE, + name: packageJSON.name + }, + true + ); // Get next version of record - const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId); - const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()); - const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch'); + const bondRecords = records.filter( + (record: any) => record.bondId === this.registryConfig.bondId + ); + const [latestBondRecord] = bondRecords.sort( + (a: any, b: any) => + new Date(b.createTime).getTime() - new Date(a.createTime).getTime() + ); + const nextVersion = semverInc( + latestBondRecord?.attributes.version ?? '0.0.0', + 'patch' + ); assert(nextVersion, 'Application record version not valid'); @@ -60,11 +82,16 @@ export class Registry { repository_ref: commitHash, repository: [repoUrl], app_type: appType, - name: packageJSON.name, + name: appName, ...(packageJSON.description && { description: packageJSON.description }), ...(packageJSON.homepage && { homepage: packageJSON.homepage }), ...(packageJSON.license && { license: packageJSON.license }), - ...(packageJSON.author && { author: typeof packageJSON.author === 'object' ? JSON.stringify(packageJSON.author) : packageJSON.author }), + ...(packageJSON.author && { + author: + typeof packageJSON.author === 'object' + ? JSON.stringify(packageJSON.author) + : packageJSON.author + }), ...(packageJSON.version && { app_version: packageJSON.version }) }; @@ -81,26 +108,45 @@ export class Registry { log('Application record data:', applicationRecord); // TODO: Discuss computation of CRN - const crn = this.getCrn(packageJSON.name); + const crn = this.getCrn(packageJSON.name, appName); log(`Setting name: ${crn} for record ID: ${result.data.id}`); - await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee); - await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee); - await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee); + await this.registry.setName( + { cid: result.data.id, crn }, + this.registryConfig.privateKey, + this.registryConfig.fee + ); + await this.registry.setName( + { cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, + this.registryConfig.privateKey, + this.registryConfig.fee + ); + await this.registry.setName( + { + cid: result.data.id, + crn: `${crn}@${applicationRecord.repository_ref}` + }, + this.registryConfig.privateKey, + this.registryConfig.fee + ); - return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord }; + return { + applicationRecordId: result.data.id, + applicationRecordData: applicationRecord + }; } async createApplicationDeploymentRequest (data: { - appName: string, - commitHash: string, - repository: string, - environmentVariables: { [key: string]: string } + appName: string; + packageJsonName: string; + commitHash: string; + repository: string; + environmentVariables: { [key: string]: string }; }): Promise<{ - applicationDeploymentRequestId: string, - applicationDeploymentRequestData: ApplicationDeploymentRequest + applicationDeploymentRequestId: string; + applicationDeploymentRequestData: ApplicationDeploymentRequest; }> { - const crn = this.getCrn(data.appName); + const crn = this.getCrn(data.packageJsonName, data.appName); const records = await this.registry.resolveNames([crn]); const applicationRecord = records[0]; @@ -124,7 +170,9 @@ export class Registry { env: data.environmentVariables }), meta: JSON.stringify({ - note: `Added by Snowball @ ${DateTime.utc().toFormat('EEE LLL dd HH:mm:ss \'UTC\' yyyy')}`, + note: `Added by Snowball @ ${DateTime.utc().toFormat( + "EEE LLL dd HH:mm:ss 'UTC' yyyy" + )}`, repository: data.repository, repository_ref: data.commitHash }) @@ -142,31 +190,40 @@ export class Registry { log(`Application deployment request record published: ${result.data.id}`); log('Application deployment request data:', applicationDeploymentRequest); - return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest }; + return { + applicationDeploymentRequestId: result.data.id, + applicationDeploymentRequestData: applicationDeploymentRequest + }; } /** * Fetch ApplicationDeploymentRecords for deployments */ - async getDeploymentRecords (deployments: Deployment[]): Promise { + async getDeploymentRecords ( + deployments: Deployment[] + ): Promise { // Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments // TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute - const records = await this.registry.queryRecords({ - type: APP_DEPLOYMENT_RECORD_TYPE - }, true); + const records = await this.registry.queryRecords( + { + type: APP_DEPLOYMENT_RECORD_TYPE + }, + true + ); // Filter records with ApplicationRecord ids - return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application)); + return records.filter((record: AppDeploymentRecord) => + deployments.some( + (deployment) => + deployment.applicationRecordId === record.attributes.application + ) + ); } - getCrn (packageJsonName: string): string { - const [arg1, arg2] = packageJsonName.split('/'); + getCrn (packageJsonName: string, appName: string): string { + const [arg1] = packageJsonName.split('/'); + const authority = arg1.replace('@', ''); - if (arg2) { - const authority = arg1.replace('@', ''); - return `crn://${authority}/applications/${arg2}`; - } - - return `crn://${arg1}/applications/${arg1}`; + return `crn://${authority}/applications/${appName}`; } } diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index f583bdab..a1c544af 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -17,7 +17,7 @@ export const createResolvers = async (service: Service): Promise => { return service.getUser(context.userId); }, - organizations: async (_:any, __: any, context: any) => { + organizations: async (_: any, __: any, context: any) => { return service.getOrganizationsByUserId(context.userId); }, @@ -25,15 +25,25 @@ export const createResolvers = async (service: Service): Promise => { return service.getProjectById(projectId); }, - projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => { - return service.getProjectsInOrganization(context.userId, organizationSlug); + projectsInOrganization: async ( + _: any, + { organizationSlug }: { organizationSlug: string }, + context: any + ) => { + return service.getProjectsInOrganization( + context.userId, + organizationSlug + ); }, deployments: async (_: any, { projectId }: { projectId: string }) => { return service.getDeploymentsByProjectId(projectId); }, - environmentVariables: async (_: any, { projectId }: { projectId: string }) => { + environmentVariables: async ( + _: any, + { projectId }: { projectId: string } + ) => { return service.getEnvironmentVariablesByProjectId(projectId); }, @@ -41,32 +51,55 @@ export const createResolvers = async (service: Service): Promise => { return service.getProjectMembersByProjectId(projectId); }, - searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => { + searchProjects: async ( + _: any, + { searchText }: { searchText: string }, + context: any + ) => { return service.searchProjects(context.userId, searchText); }, - domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere }) => { + domains: async ( + _: any, + { + projectId, + filter + }: { projectId: string; filter?: FindOptionsWhere } + ) => { return service.getDomainsByProjectId(projectId, filter); } }, // TODO: Return error in GQL response Mutation: { - removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => { + removeProjectMember: async ( + _: any, + { projectMemberId }: { projectMemberId: string }, + context: any + ) => { try { - return await service.removeProjectMember(context.userId, projectMemberId); + return await service.removeProjectMember( + context.userId, + projectMemberId + ); } catch (err) { log(err); return false; } }, - updateProjectMember: async (_: any, { projectMemberId, data }: { - projectMemberId: string, - data: { - permissions: Permission[] + updateProjectMember: async ( + _: any, + { + projectMemberId, + data + }: { + projectMemberId: string; + data: { + permissions: Permission[]; + }; } - }) => { + ) => { try { return await service.updateProjectMember(projectMemberId, data); } catch (err) { @@ -75,13 +108,19 @@ export const createResolvers = async (service: Service): Promise => { } }, - addProjectMember: async (_: any, { projectId, data }: { - projectId: string, - data: { - email: string, - permissions: Permission[] + addProjectMember: async ( + _: any, + { + projectId, + data + }: { + projectId: string; + data: { + email: string; + permissions: Permission[]; + }; } - }) => { + ) => { try { return Boolean(await service.addProjectMember(projectId, data)); } catch (err) { @@ -90,25 +129,51 @@ export const createResolvers = async (service: Service): Promise => { } }, - addEnvironmentVariables: async (_: any, { projectId, data }: { projectId: string, data: { environments: string[], key: string, value: string}[] }) => { + addEnvironmentVariables: async ( + _: any, + { + projectId, + data + }: { + projectId: string; + data: { environments: string[]; key: string; value: string }[]; + } + ) => { try { - return Boolean(await service.addEnvironmentVariables(projectId, data)); + return Boolean( + await service.addEnvironmentVariables(projectId, data) + ); } catch (err) { log(err); return false; } }, - updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial}) => { + updateEnvironmentVariable: async ( + _: any, + { + environmentVariableId, + data + }: { + environmentVariableId: string; + data: DeepPartial; + } + ) => { try { - return await service.updateEnvironmentVariable(environmentVariableId, data); + return await service.updateEnvironmentVariable( + environmentVariableId, + data + ); } catch (err) { log(err); return false; } }, - removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => { + removeEnvironmentVariable: async ( + _: any, + { environmentVariableId }: { environmentVariableId: string } + ) => { try { return await service.removeEnvironmentVariable(environmentVariableId); } catch (err) { @@ -117,25 +182,45 @@ export const createResolvers = async (service: Service): Promise => { } }, - updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => { + updateDeploymentToProd: async ( + _: any, + { deploymentId }: { deploymentId: string }, + context: any + ) => { try { - return Boolean(await service.updateDeploymentToProd(context.userId, deploymentId)); + return Boolean( + await service.updateDeploymentToProd(context.userId, deploymentId) + ); } catch (err) { log(err); return false; } }, - addProject: async (_: any, { organizationSlug, data }: { organizationSlug: string, data: DeepPartial }, context: any) => { + addProject: async ( + _: any, + { + organizationSlug, + data + }: { organizationSlug: string; data: DeepPartial }, + context: any + ) => { try { - return await service.addProject(context.userId, organizationSlug, data); + return await service.addProject( + context.userId, + organizationSlug, + data + ); } catch (err) { log(err); throw err; } }, - updateProject: async (_: any, { projectId, data }: { projectId: string, data: DeepPartial }) => { + updateProject: async ( + _: any, + { projectId, data }: { projectId: string; data: DeepPartial } + ) => { try { return await service.updateProject(projectId, data); } catch (err) { @@ -144,9 +229,15 @@ export const createResolvers = async (service: Service): Promise => { } }, - redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => { + redeployToProd: async ( + _: any, + { deploymentId }: { deploymentId: string }, + context: any + ) => { try { - return Boolean(await service.redeployToProd(context.userId, deploymentId)); + return Boolean( + await service.redeployToProd(context.userId, deploymentId) + ); } catch (err) { log(err); return false; @@ -157,7 +248,8 @@ export const createResolvers = async (service: Service): Promise => { try { return await service.deleteProject(projectId); } catch (err) { - log(err); return false; + log(err); + return false; } }, @@ -170,7 +262,13 @@ export const createResolvers = async (service: Service): Promise => { } }, - rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => { + rollbackDeployment: async ( + _: any, + { + projectId, + deploymentId + }: { deploymentId: string; projectId: string } + ) => { try { return await service.rollbackDeployment(projectId, deploymentId); } catch (err) { @@ -179,7 +277,10 @@ export const createResolvers = async (service: Service): Promise => { } }, - addDomain: async (_: any, { projectId, data }: { projectId: string, data: { name: string } }) => { + addDomain: async ( + _: any, + { projectId, data }: { projectId: string; data: { name: string } } + ) => { try { return Boolean(await service.addDomain(projectId, data)); } catch (err) { @@ -188,7 +289,10 @@ export const createResolvers = async (service: Service): Promise => { } }, - updateDomain: async (_: any, { domainId, data }: { domainId: string, data: DeepPartial}) => { + updateDomain: async ( + _: any, + { domainId, data }: { domainId: string; data: DeepPartial } + ) => { try { return await service.updateDomain(domainId, data); } catch (err) { @@ -197,7 +301,11 @@ export const createResolvers = async (service: Service): Promise => { } }, - authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => { + authenticateGitHub: async ( + _: any, + { code }: { code: string }, + context: any + ) => { try { return await service.authenticateGitHub(code, context.userId); } catch (err) { @@ -208,7 +316,9 @@ export const createResolvers = async (service: Service): Promise => { unauthenticateGitHub: async (_: any, __: object, context: any) => { try { - return service.unauthenticateGitHub(context.userId, { gitHubToken: null }); + return service.unauthenticateGitHub(context.userId, { + gitHubToken: null + }); } catch (err) { log(err); return false; diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index 620dec72..c22db8db 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -188,10 +188,19 @@ type Query { type Mutation { addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean! - updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean! + updateProjectMember( + projectMemberId: String! + data: UpdateProjectMemberInput + ): Boolean! removeProjectMember(projectMemberId: String!): Boolean! - addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean! - updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean! + addEnvironmentVariables( + projectId: String! + data: [AddEnvironmentVariableInput!] + ): Boolean! + updateEnvironmentVariable( + environmentVariableId: String! + data: UpdateEnvironmentVariableInput! + ): Boolean! removeEnvironmentVariable(environmentVariableId: String!): Boolean! updateDeploymentToProd(deploymentId: String!): Boolean! addProject(organizationSlug: String!, data: AddProjectInput): Project! diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index 0ce326ff..77f8d73b 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -15,15 +15,15 @@ import { Permission, ProjectMember } from './entity/ProjectMember'; import { User } from './entity/User'; import { Registry } from './registry'; import { GitHubConfig, RegistryConfig } from './config'; -import { AppDeploymentRecord, GitPushEventPayload } from './types'; +import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types'; const log = debug('snowball:service'); const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository'; interface Config { - gitHubConfig: GitHubConfig - registryConfig: RegistryConfig + gitHubConfig: GitHubConfig; + registryConfig: RegistryConfig; } export class Service { @@ -71,7 +71,9 @@ export class Service { }); if (deployments.length) { - log(`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`); + log( + `Found ${deployments.length} deployments in ${DeploymentStatus.Building} state` + ); // Fetch ApplicationDeploymentRecord for deployments const records = await this.registry.getDeploymentRecords(deployments); @@ -91,10 +93,12 @@ export class Service { /** * Update deployments with ApplicationDeploymentRecord data */ - async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise { + async updateDeploymentsWithRecordData ( + records: AppDeploymentRecord[] + ): Promise { // Get deployments for ApplicationDeploymentRecords const deployments = await this.db.getDeployments({ - where: records.map(record => ({ + where: records.map((record) => ({ applicationRecordId: record.attributes.application })), order: { @@ -103,38 +107,46 @@ export class Service { }); // Get project IDs of deployments that are in production environment - const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set => { - if (deployment.environment === Environment.Production) { - acc.add(deployment.projectId); - } + const productionDeploymentProjectIds = deployments.reduce( + (acc, deployment): Set => { + if (deployment.environment === Environment.Production) { + acc.add(deployment.projectId); + } - return acc; - }, new Set()); + return acc; + }, + new Set() + ); // Set old deployments isCurrent to false - await this.db.updateDeploymentsByProjectIds(Array.from(productionDeploymentProjectIds), { isCurrent: 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; - }, {}); + 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 - } - ); + 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}`); + log( + `Updated deployment ${deployment.id} with URL ${record.attributes.url}` + ); }); await Promise.all(deploymentUpdatePromises); @@ -150,7 +162,10 @@ export class Service { async getOctokit (userId: string): Promise { const user = await this.db.getUser({ where: { id: userId } }); - assert(user && user.gitHubToken, 'User needs to be authenticated with GitHub token'); + assert( + user && user.gitHubToken, + 'User needs to be authenticated with GitHub token' + ); return new Octokit({ auth: user.gitHubToken }); } @@ -165,8 +180,14 @@ export class Service { return dbProject; } - async getProjectsInOrganization (userId:string, organizationSlug: string): Promise { - const dbProjects = await this.db.getProjectsInOrganization(userId, organizationSlug); + async getProjectsInOrganization ( + userId: string, + organizationSlug: string + ): Promise { + const dbProjects = await this.db.getProjectsInOrganization( + userId, + organizationSlug + ); return dbProjects; } @@ -175,35 +196,52 @@ export class Service { return dbDeployments; } - async getEnvironmentVariablesByProjectId (projectId: string): Promise { - const dbEnvironmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId); + async getEnvironmentVariablesByProjectId ( + projectId: string + ): Promise { + const dbEnvironmentVariables = + await this.db.getEnvironmentVariablesByProjectId(projectId); return dbEnvironmentVariables; } - async getProjectMembersByProjectId (projectId: string): Promise { - const dbProjectMembers = await this.db.getProjectMembersByProjectId(projectId); + async getProjectMembersByProjectId ( + projectId: string + ): Promise { + const dbProjectMembers = + await this.db.getProjectMembersByProjectId(projectId); return dbProjectMembers; } async searchProjects (userId: string, searchText: string): Promise { - const dbProjects = await this.db.getProjectsBySearchText(userId, searchText); + const dbProjects = await this.db.getProjectsBySearchText( + userId, + searchText + ); return dbProjects; } - async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere): Promise { + async getDomainsByProjectId ( + projectId: string, + filter?: FindOptionsWhere + ): Promise { const dbDomains = await this.db.getDomainsByProjectId(projectId, filter); return dbDomains; } - async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise { + async updateProjectMember ( + projectMemberId: string, + data: { permissions: Permission[] } + ): Promise { return this.db.updateProjectMemberById(projectMemberId, data); } - async addProjectMember (projectId: string, + async addProjectMember ( + projectId: string, data: { - email: string, - permissions: Permission[] - }): Promise { + email: string; + permissions: Permission[]; + } + ): Promise { // TODO: Send invitation let user = await this.db.getUser({ where: { @@ -231,7 +269,10 @@ export class Service { return newProjectMember; } - async removeProjectMember (userId: string, projectMemberId: string): Promise { + async removeProjectMember ( + userId: string, + projectMemberId: string + ): Promise { const member = await this.db.getProjectMemberById(projectMemberId); if (String(member.member.id) === userId) { @@ -248,33 +289,48 @@ export class Service { } } - async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise { - 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 - }) + async addEnvironmentVariables ( + projectId: string, + data: { environments: string[]; key: string; value: string }[] + ): Promise { + 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(); + }) + .flat(); - const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables); + const savedEnvironmentVariables = await this.db.addEnvironmentVariables( + formattedEnvironmentVariables + ); return savedEnvironmentVariables; } - async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial): Promise { + async updateEnvironmentVariable ( + environmentVariableId: string, + data: DeepPartial + ): Promise { return this.db.updateEnvironmentVariable(environmentVariableId, data); } - async removeEnvironmentVariable (environmentVariableId: string): Promise { + async removeEnvironmentVariable ( + environmentVariableId: string + ): Promise { return this.db.deleteEnvironmentVariable(environmentVariableId); } - async updateDeploymentToProd (userId: string, deploymentId: string): Promise { + async updateDeploymentToProd ( + userId: string, + deploymentId: string + ): Promise { const oldDeployment = await this.db.getDeployment({ where: { id: deploymentId }, relations: { @@ -286,20 +342,21 @@ export class Service { throw new Error('Deployment does not exist'); } - const prodBranchDomains = await this.db.getDomainsByProjectId(oldDeployment.project.id, { branch: oldDeployment.project.prodBranch }); + const prodBranchDomains = await this.db.getDomainsByProjectId( + oldDeployment.project.id, + { branch: oldDeployment.project.prodBranch } + ); const octokit = await this.getOctokit(userId); - const newDeployment = await this.createDeployment(userId, - octokit, - { - project: oldDeployment.project, - branch: oldDeployment.branch, - environment: Environment.Production, - domain: prodBranchDomains[0], - commitHash: oldDeployment.commitHash, - commitMessage: oldDeployment.commitMessage - }); + const newDeployment = await this.createDeployment(userId, octokit, { + project: oldDeployment.project, + branch: oldDeployment.branch, + environment: Environment.Production, + domain: prodBranchDomains[0], + commitHash: oldDeployment.commitHash, + commitMessage: oldDeployment.commitMessage + }); return newDeployment; } @@ -311,7 +368,9 @@ export class Service { recordData: { repoUrl?: string } = {} ): Promise { assert(data.project?.repository, 'Project repository not found'); - log(`Creating deployment in project ${data.project.name} from branch ${data.branch}`); + 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({ @@ -326,28 +385,61 @@ export class Service { } assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file'); - const packageJSON = JSON.parse(atob(packageJSONData.content)); + const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content)); + + assert(packageJSON.name, "name field doesn't exist in package.json"); if (!recordData.repoUrl) { - const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo }); + const { data: repoDetails } = await octokit.rest.repos.get({ + owner, + repo + }); recordData.repoUrl = repoDetails.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({ - packageJSON, - appType: data.project!.template!, - commitHash: data.commitHash!, - repoUrl: recordData.repoUrl - }); + const { applicationRecordId, applicationRecordData } = + await this.registry.createApplicationRecord({ + appName: repo, + packageJSON, + appType: data.project!.template!, + commitHash: data.commitHash!, + repoUrl: recordData.repoUrl + }); + + 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 } + ); + + const { applicationDeploymentRequestId, applicationDeploymentRequestData } = + await this.registry.createApplicationDeploymentRequest({ + appName: repo, + packageJsonName: packageJSON.name, + commitHash: data.commitHash!, + repository: recordData.repoUrl, + environmentVariables: environmentVariablesObj + }); // Update previous deployment with prod branch domain // TODO: Fix unique constraint error for domain - await this.db.updateDeployment({ - domainId: data.domain?.id - }, { - domain: null - }); + await this.db.updateDeployment( + { + domainId: data.domain?.id + }, + { + domain: null + } + ); const newDeployment = await this.db.addDeployment({ project: data.project, @@ -358,17 +450,25 @@ export class Service { status: DeploymentStatus.Building, applicationRecordId, applicationRecordData, + applicationDeploymentRequestId, + applicationDeploymentRequestData, domain: data.domain, createdBy: Object.assign(new User(), { id: userId }) }); - log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`); + log( + `Created deployment ${newDeployment.id} and published application record ${applicationRecordId}` + ); return newDeployment; } - async addProject (userId: string, organizationSlug: string, data: DeepPartial): Promise { + async addProject ( + userId: string, + organizationSlug: string, + data: DeepPartial + ): Promise { const organization = await this.db.getOrganization({ where: { slug: organizationSlug @@ -383,7 +483,9 @@ export class Service { const octokit = await this.getOctokit(userId); const [owner, repo] = project.repository.split('/'); - const { data: [latestCommit] } = await octokit.rest.repos.listCommits({ + const { + data: [latestCommit] + } = await octokit.rest.repos.listCommits({ owner, repo, sha: project.prodBranch, @@ -393,7 +495,8 @@ export class Service { const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo }); // Create deployment with prod branch and latest commit - const newDeployment = await this.createDeployment(userId, + await this.createDeployment( + userId, octokit, { project, @@ -408,27 +511,6 @@ export class Service { } ); - const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(project.id, { environment: Environment.Production }); - - const environmentVariablesObj = environmentVariables.reduce((acc, env) => { - acc[env.key] = env.value; - - return acc; - }, {} as { [key: string]: string }); - - const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest( - { - appName: newDeployment.applicationRecordData.name!, - commitHash: latestCommit.sha, - repository: repoDetails.html_url, - environmentVariables: environmentVariablesObj - }); - - await this.db.updateProjectById(project.id, { - applicationDeploymentRequestId, - applicationDeploymentRequestData - }); - await this.createRepoHook(octokit, project); return project; @@ -441,7 +523,10 @@ export class Service { owner, repo, config: { - url: new URL('api/github/webhook', this.config.gitHubConfig.webhookUrl).href, + url: new URL( + 'api/github/webhook', + this.config.gitHubConfig.webhookUrl + ).href, content_type: 'json' }, events: ['push'] @@ -449,9 +534,13 @@ export class Service { } 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)) + !( + err instanceof RequestError && + err.status === 422 && + (err.response?.data as any).errors.some( + (err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR + ) + ) ) { throw err; } @@ -463,7 +552,9 @@ export class Service { async handleGitHubPush (data: GitPushEventPayload): Promise { const { repository, ref, head_commit: headCommit } = data; log(`Handling GitHub push event from repository: ${repository.full_name}`); - const projects = await this.db.getProjects({ where: { repository: repository.full_name } }); + const projects = await this.db.getProjects({ + where: { repository: repository.full_name } + }); if (!projects.length) { log(`No projects found for repository ${repository.full_name}`); @@ -475,23 +566,29 @@ export class Service { for await (const project of projects) { const octokit = await this.getOctokit(project.ownerId); - const [domain] = await this.db.getDomainsByProjectId(project.id, { branch }); + 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 - }); + 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): Promise { + async updateProject ( + projectId: string, + data: DeepPartial + ): Promise { return this.db.updateProjectById(projectId, data); } @@ -508,13 +605,18 @@ export class Service { }); if (domainsRedirectedFrom.length > 0) { - throw new Error('Cannot delete domain since it has redirects from other domains'); + throw new Error( + 'Cannot delete domain since it has redirects from other domains' + ); } return this.db.deleteDomainById(domainId); } - async redeployToProd (userId: string, deploymentId: string): Promise { + async redeployToProd ( + userId: string, + deploymentId: string + ): Promise { const oldDeployment = await this.db.getDeployment({ relations: { project: true, @@ -532,22 +634,23 @@ export class Service { const octokit = await this.getOctokit(userId); - const newDeployment = await this.createDeployment(userId, - 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 - }); + const newDeployment = await this.createDeployment(userId, 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 { + async rollbackDeployment ( + projectId: string, + deploymentId: string + ): Promise { // TODO: Implement transactions const oldCurrentDeployment = await this.db.getDeployment({ relations: { @@ -565,16 +668,25 @@ export class Service { throw new Error('Current deployment doesnot exist'); } - const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null }); + const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById( + oldCurrentDeployment.id, + { isCurrent: false, domain: null } + ); - const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(deploymentId, { isCurrent: true, domain: oldCurrentDeployment?.domain }); + const newCurrentDeploymentUpdate = await this.db.updateDeploymentById( + deploymentId, + { isCurrent: true, domain: oldCurrentDeployment?.domain } + ); return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate; } - async addDomain (projectId: string, data: { name: string }): Promise<{ - primaryDomain: Domain, - redirectedDomain: Domain + async addDomain ( + projectId: string, + data: { name: string } + ): Promise<{ + primaryDomain: Domain; + redirectedDomain: Domain; }> { const currentProject = await this.db.getProjectById(projectId); @@ -599,12 +711,20 @@ export class Service { redirectTo: savedPrimaryDomain }; - const savedRedirectedDomain = await this.db.addDomain(redirectedDomainDetails); + const savedRedirectedDomain = await this.db.addDomain( + redirectedDomainDetails + ); - return { primaryDomain: savedPrimaryDomain, redirectedDomain: savedRedirectedDomain }; + return { + primaryDomain: savedPrimaryDomain, + redirectedDomain: savedRedirectedDomain + }; } - async updateDomain (domainId: string, data: DeepPartial): Promise { + async updateDomain ( + domainId: string, + data: DeepPartial + ): Promise { const domain = await this.db.getDomain({ where: { id: domainId @@ -645,7 +765,9 @@ export class Service { } if (redirectedDomain.redirectToId) { - throw new Error('Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.'); + throw new Error( + 'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.' + ); } newDomain.redirectTo = redirectedDomain; @@ -656,8 +778,13 @@ export class Service { return updateResult; } - async authenticateGitHub (code:string, userId: string): Promise<{token: string}> { - const { authentication: { token } } = await this.oauthApp.createToken({ + async authenticateGitHub ( + code: string, + userId: string + ): Promise<{ token: string }> { + const { + authentication: { token } + } = await this.oauthApp.createToken({ code }); @@ -666,7 +793,10 @@ export class Service { return { token }; } - async unauthenticateGitHub (userId: string, data: DeepPartial): Promise { + async unauthenticateGitHub ( + userId: string, + data: DeepPartial + ): Promise { return this.db.updateUser(userId, data); } } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 71626645..eae6929f 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -1,6 +1,6 @@ export interface PackageJSON { - name?: string; - version?: string; + name: string; + version: string; author?: string; description?: string; homepage?: string; @@ -47,5 +47,5 @@ interface RegistryRecord { } export interface AppDeploymentRecord extends RegistryRecord { - attributes: AppDeploymentRecordAttributes + attributes: AppDeploymentRecordAttributes; } diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index f33963f9..693223bb 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -2,6 +2,7 @@ import fs from 'fs-extra'; import path from 'path'; import toml from 'toml'; import debug from 'debug'; +import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm'; const log = debug('snowball:utils'); @@ -19,3 +20,49 @@ export const getConfig = async ( return config; }; + +export const checkFileExists = async (filePath: string): Promise => { + try { + await fs.access(filePath, fs.constants.F_OK); + return true; + } catch (err) { + log(err); + return false; + } +}; + +export const getEntities = async (filePath: string): Promise => { + const entitiesData = await fs.readFile(filePath, 'utf-8'); + const entities = JSON.parse(entitiesData); + return entities; +}; + +export const loadAndSaveData = async ( + entityType: EntityTarget, + dataSource: DataSource, + entities: any, + relations?: any | undefined +): Promise => { + const entityRepository = dataSource.getRepository(entityType); + + const savedEntity: Entity[] = []; + + for (const entityData of entities) { + let entity = entityRepository.create(entityData as DeepPartial); + + if (relations) { + for (const field in relations) { + const valueIndex = String(field + 'Index'); + + entity = { + ...entity, + [field]: relations[field][entityData[valueIndex]] + }; + } + } + const dbEntity = await entityRepository.save(entity); + savedEntity.push(dbEntity); + } + + return savedEntity; +}; diff --git a/packages/backend/test/delete-db.ts b/packages/backend/test/delete-db.ts index 9fd6a9ab..d984fc8c 100644 --- a/packages/backend/test/delete-db.ts +++ b/packages/backend/test/delete-db.ts @@ -18,4 +18,4 @@ const main = async () => { deleteFile(config.database.dbPath); }; -main().catch(err => log(err)); +main().catch((err) => log(err)); diff --git a/packages/backend/test/fixtures/deployments.json b/packages/backend/test/fixtures/deployments.json index 5eb682a8..b73956f8 100644 --- a/packages/backend/test/fixtures/deployments.json +++ b/packages/backend/test/fixtures/deployments.json @@ -1,14 +1,16 @@ [ { "projectIndex": 0, - "domainIndex":0, + "domainIndex": 0, "createdByIndex": 0, - "id":"ffhae3zq", + "id": "ffhae3zq", "status": "Ready", "environment": "Production", "isCurrent": true, "applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "main", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -16,14 +18,16 @@ }, { "projectIndex": 0, - "domainIndex":1, + "domainIndex": 1, "createdByIndex": 0, - "id":"vehagei8", + "id": "vehagei8", "status": "Ready", "environment": "Preview", "isCurrent": false, "applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -31,14 +35,16 @@ }, { "projectIndex": 0, - "domainIndex":2, + "domainIndex": 2, "createdByIndex": 0, - "id":"qmgekyte", + "id": "qmgekyte", "status": "Ready", "environment": "Development", "isCurrent": false, "applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -48,12 +54,14 @@ "projectIndex": 0, "domainIndex": null, "createdByIndex": 0, - "id":"f8wsyim6", + "id": "f8wsyim6", "status": "Ready", "environment": "Production", "isCurrent": false, "applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "prod", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -61,14 +69,16 @@ }, { "projectIndex": 1, - "domainIndex":3, + "domainIndex": 3, "createdByIndex": 1, - "id":"eO8cckxk", + "id": "eO8cckxk", "status": "Ready", "environment": "Production", "isCurrent": true, "applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "main", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -76,14 +86,16 @@ }, { "projectIndex": 1, - "domainIndex":4, + "domainIndex": 4, "createdByIndex": 1, - "id":"yaq0t5yw", + "id": "yaq0t5yw", "status": "Ready", "environment": "Preview", "isCurrent": false, "applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -91,14 +103,16 @@ }, { "projectIndex": 1, - "domainIndex":5, + "domainIndex": 5, "createdByIndex": 1, - "id":"hwwr6sbx", + "id": "hwwr6sbx", "status": "Ready", "environment": "Development", "isCurrent": false, "applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -106,14 +120,16 @@ }, { "projectIndex": 2, - "domainIndex":9, + "domainIndex": 9, "createdByIndex": 2, - "id":"ndxje48a", + "id": "ndxje48a", "status": "Ready", "environment": "Production", "isCurrent": true, "applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "main", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -121,14 +137,16 @@ }, { "projectIndex": 2, - "domainIndex":7, + "domainIndex": 7, "createdByIndex": 2, - "id":"gtgpgvei", + "id": "gtgpgvei", "status": "Ready", "environment": "Preview", "isCurrent": false, "applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -136,14 +154,16 @@ }, { "projectIndex": 2, - "domainIndex":8, + "domainIndex": 8, "createdByIndex": 2, - "id":"b4bpthjr", + "id": "b4bpthjr", "status": "Ready", "environment": "Development", "isCurrent": false, "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", "applicationRecordData": {}, + "applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -153,12 +173,14 @@ "projectIndex": 3, "domainIndex": 6, "createdByIndex": 2, - "id":"b4bpthjr", + "id": "b4bpthjr", "status": "Ready", "environment": "Production", "isCurrent": true, "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", "applicationRecordData": {}, + "applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", diff --git a/packages/backend/test/fixtures/project-members.json b/packages/backend/test/fixtures/project-members.json index 7b2661ea..84029c27 100644 --- a/packages/backend/test/fixtures/project-members.json +++ b/packages/backend/test/fixtures/project-members.json @@ -2,77 +2,55 @@ { "memberIndex": 1, "projectIndex": 0, - "permissions": [ - "View" - ], + "permissions": ["View"], "isPending": false }, { "memberIndex": 2, "projectIndex": 0, - "permissions": [ - "View", - "Edit" - ], + "permissions": ["View", "Edit"], "isPending": false }, { "memberIndex": 2, "projectIndex": 1, - "permissions": [ - "View" - ], + "permissions": ["View"], "isPending": false }, { "memberIndex": 0, "projectIndex": 2, - "permissions": [ - "View" - ], + "permissions": ["View"], "isPending": false }, { "memberIndex": 1, "projectIndex": 2, - "permissions": [ - "View", - "Edit" - ], + "permissions": ["View", "Edit"], "isPending": false }, { "memberIndex": 0, "projectIndex": 3, - "permissions": [ - "View" - ], + "permissions": ["View"], "isPending": false }, { "memberIndex": 2, "projectIndex": 3, - "permissions": [ - "View", - "Edit" - ], + "permissions": ["View", "Edit"], "isPending": false }, { "memberIndex": 1, "projectIndex": 4, - "permissions": [ - "View" - ], + "permissions": ["View"], "isPending": false }, { "memberIndex": 2, "projectIndex": 4, - "permissions": [ - "View", - "Edit" - ], + "permissions": ["View", "Edit"], "isPending": false } ] diff --git a/packages/backend/test/fixtures/projects.json b/packages/backend/test/fixtures/projects.json index 55c5ee23..f4564643 100644 --- a/packages/backend/test/fixtures/projects.json +++ b/packages/backend/test/fixtures/projects.json @@ -10,8 +10,6 @@ "framework": "test", "webhooks": [], "icon": "", - "applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, "subDomain": "testProject.snowball.xyz" }, { @@ -25,8 +23,6 @@ "framework": "test-2", "webhooks": [], "icon": "", - "applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, "subDomain": "testProject-2.snowball.xyz" }, { @@ -40,8 +36,6 @@ "framework": "test-3", "webhooks": [], "icon": "", - "applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, "subDomain": "iglootools.snowball.xyz" }, { @@ -55,8 +49,6 @@ "framework": "test-4", "webhooks": [], "icon": "", - "applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, "subDomain": "iglootools-2.snowball.xyz" }, { @@ -70,8 +62,6 @@ "framework": "test-5", "webhooks": [], "icon": "", - "applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, "subDomain": "snowball-2.snowball.xyz" } ] diff --git a/packages/backend/test/initialize-db.ts b/packages/backend/test/initialize-db.ts index 0aade587..c4384294 100644 --- a/packages/backend/test/initialize-db.ts +++ b/packages/backend/test/initialize-db.ts @@ -1,5 +1,4 @@ -import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm'; -import * as fs from 'fs/promises'; +import { DataSource } from 'typeorm'; import debug from 'debug'; import path from 'path'; @@ -11,7 +10,12 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable'; import { Domain } from '../src/entity/Domain'; import { ProjectMember } from '../src/entity/ProjectMember'; import { Deployment } from '../src/entity/Deployment'; -import { getConfig } from '../src/utils'; +import { + checkFileExists, + getConfig, + getEntities, + loadAndSaveData +} from '../src/utils'; import { Config } from '../src/config'; import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants'; @@ -27,43 +31,35 @@ const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json'; const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json'; const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json'; -const loadAndSaveData = async (entityType: EntityTarget, dataSource: DataSource, filePath: string, relations?: any | undefined) => { - const entitiesData = await fs.readFile(filePath, 'utf-8'); - const entities = JSON.parse(entitiesData); - const entityRepository = dataSource.getRepository(entityType); - - const savedEntity:Entity[] = []; - - for (const entityData of entities) { - let entity = entityRepository.create(entityData as DeepPartial); - - if (relations) { - for (const field in relations) { - const valueIndex = String(field + 'Index'); - - entity = { - ...entity, - [field]: relations[field][entityData[valueIndex]] - }; - } - } - const dbEntity = await entityRepository.save(entity); - savedEntity.push(dbEntity); - } - - return savedEntity; -}; - const generateTestData = async (dataSource: DataSource) => { - const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH)); - const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH)); + const userEntities = await getEntities( + path.resolve(__dirname, USER_DATA_PATH) + ); + const savedUsers = await loadAndSaveData(User, dataSource, userEntities); + + const orgEntities = await getEntities( + path.resolve(__dirname, ORGANIZATION_DATA_PATH) + ); + const savedOrgs = await loadAndSaveData( + Organization, + dataSource, + orgEntities + ); const projectRelations = { owner: savedUsers, organization: savedOrgs }; - const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations); + const projectEntities = await getEntities( + path.resolve(__dirname, PROJECT_DATA_PATH) + ); + const savedProjects = await loadAndSaveData( + Project, + dataSource, + projectEntities, + projectRelations + ); const domainRepository = dataSource.getRepository(Domain); @@ -71,14 +67,30 @@ const generateTestData = async (dataSource: DataSource) => { project: savedProjects }; - const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations); + const primaryDomainsEntities = await getEntities( + path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH) + ); + const savedPrimaryDomains = await loadAndSaveData( + Domain, + dataSource, + primaryDomainsEntities, + domainPrimaryRelations + ); const domainRedirectedRelations = { project: savedProjects, redirectTo: savedPrimaryDomains }; - await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations); + const redirectDomainsEntities = await getEntities( + path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH) + ); + await loadAndSaveData( + Domain, + dataSource, + redirectDomainsEntities, + domainRedirectedRelations + ); const savedDomains = await domainRepository.find(); @@ -87,14 +99,30 @@ const generateTestData = async (dataSource: DataSource) => { organization: savedOrgs }; - await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations); + const userOrganizationsEntities = await getEntities( + path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH) + ); + await loadAndSaveData( + UserOrganization, + dataSource, + userOrganizationsEntities, + userOrganizationRelations + ); const projectMemberRelations = { member: savedUsers, project: savedProjects }; - await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations); + const projectMembersEntities = await getEntities( + path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH) + ); + await loadAndSaveData( + ProjectMember, + dataSource, + projectMembersEntities, + projectMemberRelations + ); const deploymentRelations = { project: savedProjects, @@ -102,23 +130,29 @@ const generateTestData = async (dataSource: DataSource) => { createdBy: savedUsers }; - await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations); + const deploymentsEntities = await getEntities( + path.resolve(__dirname, DEPLOYMENT_DATA_PATH) + ); + await loadAndSaveData( + Deployment, + dataSource, + deploymentsEntities, + deploymentRelations + ); const environmentVariableRelations = { project: savedProjects }; - await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations); -}; - -const checkFileExists = async (filePath: string) => { - try { - await fs.access(filePath, fs.constants.F_OK); - return true; - } catch (err) { - log(err); - return false; - } + const environmentVariablesEntities = await getEntities( + path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH) + ); + await loadAndSaveData( + EnvironmentVariable, + dataSource, + environmentVariablesEntities, + environmentVariableRelations + ); }; const main = async () => { diff --git a/packages/backend/test/initialize-registry.ts b/packages/backend/test/initialize-registry.ts index 9162f718..8a91bb53 100644 --- a/packages/backend/test/initialize-registry.ts +++ b/packages/backend/test/initialize-registry.ts @@ -17,16 +17,32 @@ const AUTHORITY_NAMES = ['snowballtools', 'cerc-io']; async function main () { const { registryConfig } = await getConfig(DEFAULT_CONFIG_FILE_PATH); - const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId); + const registry = new Registry( + registryConfig.gqlEndpoint, + registryConfig.restEndpoint, + registryConfig.chainId + ); const bondId = await registry.getNextBondId(registryConfig.privateKey); log('bondId:', bondId); - await registry.createBond({ denom: DENOM, amount: BOND_AMOUNT }, registryConfig.privateKey, registryConfig.fee); + await registry.createBond( + { denom: DENOM, amount: BOND_AMOUNT }, + registryConfig.privateKey, + registryConfig.fee + ); for await (const name of AUTHORITY_NAMES) { - await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee); + await registry.reserveAuthority( + { name }, + registryConfig.privateKey, + registryConfig.fee + ); log('Reserved authority name:', name); - await registry.setAuthorityBond({ name, bondId }, registryConfig.privateKey, registryConfig.fee); + await registry.setAuthorityBond( + { name, bondId }, + registryConfig.privateKey, + registryConfig.fee + ); log(`Bond ${bondId} set for authority ${name}`); } } diff --git a/packages/backend/test/publish-deploy-records.ts b/packages/backend/test/publish-deploy-records.ts index f5636c4e..e963e317 100644 --- a/packages/backend/test/publish-deploy-records.ts +++ b/packages/backend/test/publish-deploy-records.ts @@ -12,9 +12,15 @@ import { Deployment, DeploymentStatus } from '../src/entity/Deployment'; const log = debug('snowball:publish-deploy-records'); async function main () { - const { registryConfig, database } = await getConfig(DEFAULT_CONFIG_FILE_PATH); + const { registryConfig, database } = await getConfig( + DEFAULT_CONFIG_FILE_PATH + ); - const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId); + const registry = new Registry( + registryConfig.gqlEndpoint, + registryConfig.restEndpoint, + registryConfig.chainId + ); const dataSource = new DataSource({ type: 'better-sqlite3', @@ -53,7 +59,7 @@ async function main () { so: '66fcfa49a1664d4cb4ce4f72c1c0e151' }), - request: deployment.project.applicationDeploymentRequestId, + request: deployment.applicationDeploymentRequestId, url }; diff --git a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx index 958382ac..1d9e4e2d 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx @@ -16,6 +16,7 @@ const DEFAULT_FILTER_VALUE: FilterValue = { searchedBranch: '', status: StatusOptions.ALL_STATUS, }; +const FETCH_DEPLOYMENTS_INTERVAL = 5000; const DeploymentsTabPanel = () => { const client = useGQLClient(); @@ -26,22 +27,30 @@ const DeploymentsTabPanel = () => { const [deployments, setDeployments] = useState([]); const [prodBranchDomains, setProdBranchDomains] = useState([]); - const fetchDeployments = async () => { + const fetchDeployments = useCallback(async () => { const { deployments } = await client.getDeployments(project.id); setDeployments(deployments); - }; + }, [client, project]); const fetchProductionBranchDomains = useCallback(async () => { const { domains } = await client.getDomains(project.id, { branch: project.prodBranch, }); setProdBranchDomains(domains); - }, []); + }, [client, project]); useEffect(() => { - fetchDeployments(); fetchProductionBranchDomains(); - }, []); + fetchDeployments(); + + const interval = setInterval(() => { + fetchDeployments(); + }, FETCH_DEPLOYMENTS_INTERVAL); + + return () => { + clearInterval(interval); + }; + }, [fetchDeployments, fetchProductionBranchDomains]); const currentDeployment = useMemo(() => { return deployments.find((deployment) => {