Merge branch 'main' of https://github.com/snowball-tools/snowballtools-base into andrehadianto/design-system-components

This commit is contained in:
Wahyu Kurniawan 2024-02-22 12:45:17 +07:00
commit 496404195c
No known key found for this signature in database
GPG Key ID: 040A1549143A8E33
26 changed files with 1057 additions and 455 deletions

26
build-webapp.sh Executable file
View File

@ -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 <<EOF
REACT_APP_GQL_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_gql_url'
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
REACT_APP_GITHUB_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_templaterepo'
EOF
yarn || exit 1
yarn build || exit 1
if [[ ! -d "$OUTPUT_DIR" ]]; then
echo "Missing output directory: $OUTPUT_DIR" 1>&2
exit 1
fi
mv "$OUTPUT_DIR" "$DEST_DIR"

View File

@ -25,6 +25,9 @@
"allowArgumentsExplicitlyTypedAsAny": true "allowArgumentsExplicitlyTypedAsAny": true
} }
], ],
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }] "@typescript-eslint/no-unused-vars": [
"error",
{ "ignoreRestSiblings": true }
]
} }
} }

View File

@ -13,7 +13,7 @@ export interface GitHubConfig {
oAuth: { oAuth: {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
} };
} }
export interface RegistryConfig { export interface RegistryConfig {
@ -27,7 +27,7 @@ export interface RegistryConfig {
amount: string; amount: string;
denom: string; denom: string;
gas: string; gas: string;
} };
} }
export interface Config { export interface Config {

View File

@ -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'; export const DEFAULT_GQL_PATH = '/graphql';
// Note: temporary hardcoded user, later to be derived from auth token // 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';

View File

@ -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 path from 'path';
import debug from 'debug'; import debug from 'debug';
import assert from 'assert'; import assert from 'assert';
@ -14,6 +20,12 @@ import { ProjectMember } from './entity/ProjectMember';
import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { PROJECT_DOMAIN } from './constants'; 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'); const log = debug('snowball:database');
@ -36,6 +48,40 @@ export class Database {
async init (): Promise<void> { async init (): Promise<void> {
await this.dataSource.initialize(); await this.dataSource.initialize();
log('database initialized'); 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<User>): Promise<User | null> { async getUser (options: FindOneOptions<User>): Promise<User | null> {
@ -60,7 +106,18 @@ export class Database {
return updateResult.affected > 0; return updateResult.affected > 0;
} }
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> { async getOrganizations (
options: FindManyOptions<Organization>
): Promise<Organization[]> {
const organizationRepository = this.dataSource.getRepository(Organization);
const organizations = await organizationRepository.find(options);
return organizations;
}
async getOrganization (
options: FindOneOptions<Organization>
): Promise<Organization | null> {
const organizationRepository = this.dataSource.getRepository(Organization); const organizationRepository = this.dataSource.getRepository(Organization);
const organization = await organizationRepository.findOne(options); const organization = await organizationRepository.findOne(options);
@ -95,7 +152,11 @@ export class Database {
const project = await projectRepository const project = await projectRepository
.createQueryBuilder('project') .createQueryBuilder('project')
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true') .leftJoinAndSelect(
'project.deployments',
'deployments',
'deployments.isCurrent = true'
)
.leftJoinAndSelect('deployments.createdBy', 'user') .leftJoinAndSelect('deployments.createdBy', 'user')
.leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('deployments.domain', 'domain')
.leftJoinAndSelect('project.owner', 'owner') .leftJoinAndSelect('project.owner', 'owner')
@ -108,19 +169,29 @@ export class Database {
return project; return project;
} }
async getProjectsInOrganization (userId: string, organizationSlug: string): Promise<Project[]> { async getProjectsInOrganization (
userId: string,
organizationSlug: string
): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository const projects = await projectRepository
.createQueryBuilder('project') .createQueryBuilder('project')
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true') .leftJoinAndSelect(
'project.deployments',
'deployments',
'deployments.isCurrent = true'
)
.leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('deployments.domain', 'domain')
.leftJoin('project.projectMembers', 'projectMembers') .leftJoin('project.projectMembers', 'projectMembers')
.leftJoin('project.organization', 'organization') .leftJoin('project.organization', 'organization')
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', { .where(
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug',
{
userId, userId,
organizationSlug organizationSlug
}) }
)
.getMany(); .getMany();
return projects; return projects;
@ -129,7 +200,9 @@ export class Database {
/** /**
* Get deployments with specified filter * Get deployments with specified filter
*/ */
async getDeployments (options: FindManyOptions<Deployment>): Promise<Deployment[]> { async getDeployments (
options: FindManyOptions<Deployment>
): Promise<Deployment[]> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployments = await deploymentRepository.find(options); const deployments = await deploymentRepository.find(options);
@ -154,7 +227,9 @@ export class Database {
}); });
} }
async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> { async getDeployment (
options: FindOneOptions<Deployment>
): Promise<Deployment | null> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.findOne(options); const deployment = await deploymentRepository.findOne(options);
@ -182,8 +257,11 @@ export class Database {
return deployment; return deployment;
} }
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> { async getProjectMembersByProjectId (
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); projectId: string
): Promise<ProjectMember[]> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const projectMembers = await projectMemberRepository.find({ const projectMembers = await projectMemberRepository.find({
relations: { relations: {
@ -200,8 +278,12 @@ export class Database {
return projectMembers; return projectMembers;
} }
async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere<EnvironmentVariable>): Promise<EnvironmentVariable[]> { async getEnvironmentVariablesByProjectId (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); projectId: string,
filter?: FindOptionsWhere<EnvironmentVariable>
): Promise<EnvironmentVariable[]> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const environmentVariables = await environmentVariableRepository.find({ const environmentVariables = await environmentVariableRepository.find({
where: { where: {
@ -216,9 +298,12 @@ export class Database {
} }
async removeProjectMemberById (projectMemberId: string): Promise<boolean> { async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
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) { if (deleteResult.affected) {
return deleteResult.affected > 0; return deleteResult.affected > 0;
@ -227,37 +312,63 @@ export class Database {
} }
} }
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> { async updateProjectMemberById (
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); projectMemberId: string,
const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data); data: DeepPartial<ProjectMember>
): Promise<boolean> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const updateResult = await projectMemberRepository.update(
{ id: projectMemberId },
data
);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> { async addProjectMember (
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); data: DeepPartial<ProjectMember>
): Promise<ProjectMember> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const newProjectMember = await projectMemberRepository.save(data); const newProjectMember = await projectMemberRepository.save(data);
return newProjectMember; return newProjectMember;
} }
async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> { async addEnvironmentVariables (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); data: DeepPartial<EnvironmentVariable>[]
const savedEnvironmentVariables = await environmentVariableRepository.save(data); ): Promise<EnvironmentVariable[]> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const savedEnvironmentVariables =
await environmentVariableRepository.save(data);
return savedEnvironmentVariables; return savedEnvironmentVariables;
} }
async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> { async updateEnvironmentVariable (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); environmentVariableId: string,
const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data); data: DeepPartial<EnvironmentVariable>
): Promise<boolean> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const updateResult = await environmentVariableRepository.update(
{ id: environmentVariableId },
data
);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> { async deleteEnvironmentVariable (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); environmentVariableId: string
const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId }); ): Promise<boolean> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const deleteResult = await environmentVariableRepository.delete({
id: environmentVariableId
});
if (deleteResult.affected) { if (deleteResult.affected) {
return deleteResult.affected > 0; return deleteResult.affected > 0;
@ -267,7 +378,8 @@ export class Database {
} }
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> { async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const projectMemberWithProject = await projectMemberRepository.find({ const projectMemberWithProject = await projectMemberRepository.find({
relations: { relations: {
@ -279,8 +391,7 @@ export class Database {
where: { where: {
id: projectMemberId id: projectMemberId
} }
} });
);
if (projectMemberWithProject.length === 0) { if (projectMemberWithProject.length === 0) {
throw new Error('Member does not exist'); throw new Error('Member does not exist');
@ -289,34 +400,49 @@ export class Database {
return projectMemberWithProject[0]; return projectMemberWithProject[0];
} }
async getProjectsBySearchText (userId: string, searchText: string): Promise<Project[]> { async getProjectsBySearchText (
userId: string,
searchText: string
): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository const projects = await projectRepository
.createQueryBuilder('project') .createQueryBuilder('project')
.leftJoinAndSelect('project.organization', 'organization') .leftJoinAndSelect('project.organization', 'organization')
.leftJoin('project.projectMembers', 'projectMembers') .leftJoin('project.projectMembers', 'projectMembers')
.where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', { .where(
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText',
{
userId, userId,
searchText: `%${searchText}%` searchText: `%${searchText}%`
}) }
)
.getMany(); .getMany();
return projects; return projects;
} }
async updateDeploymentById (deploymentId: string, data: DeepPartial<Deployment>): Promise<boolean> { async updateDeploymentById (
deploymentId: string,
data: DeepPartial<Deployment>
): Promise<boolean> {
return this.updateDeployment({ id: deploymentId }, data); return this.updateDeployment({ id: deploymentId }, data);
} }
async updateDeployment (criteria: FindOptionsWhere<Deployment>, data: DeepPartial<Deployment>): Promise<boolean> { async updateDeployment (
criteria: FindOptionsWhere<Deployment>,
data: DeepPartial<Deployment>
): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const updateResult = await deploymentRepository.update(criteria, data); const updateResult = await deploymentRepository.update(criteria, data);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> { async updateDeploymentsByProjectIds (
projectIds: string[],
data: DeepPartial<Deployment>
): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const updateResult = await deploymentRepository const updateResult = await deploymentRepository
@ -329,7 +455,11 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async addProject (userId: string, organizationId: string, data: DeepPartial<Project>): Promise<Project> { async addProject (
userId: string,
organizationId: string,
data: DeepPartial<Project>
): Promise<Project> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
// TODO: Check if organization exists // TODO: Check if organization exists
@ -352,9 +482,15 @@ export class Database {
return projectRepository.save(newProject); return projectRepository.save(newProject);
} }
async updateProjectById (projectId: string, data: DeepPartial<Project>): Promise<boolean> { async updateProjectById (
projectId: string,
data: DeepPartial<Project>
): Promise<boolean> {
const projectRepository = this.dataSource.getRepository(Project); 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); return Boolean(updateResult.affected);
} }
@ -401,14 +537,20 @@ export class Database {
return domain; return domain;
} }
async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> { async updateDomainById (
domainId: string,
data: DeepPartial<Domain>
): Promise<boolean> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const updateResult = await domainRepository.update({ id: domainId }, data); const updateResult = await domainRepository.update({ id: domainId }, data);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> { async getDomainsByProjectId (
projectId: string,
filter?: FindOptionsWhere<Domain>
): Promise<Domain[]> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const domains = await domainRepository.find({ const domains = await domainRepository.find({

View File

@ -26,18 +26,27 @@ export enum DeploymentStatus {
Error = 'Error', Error = 'Error',
} }
export interface ApplicationDeploymentRequest {
type: string;
version: string;
name: string;
application: string;
config: string;
meta: string;
}
export interface ApplicationRecord { export interface ApplicationRecord {
type: string; type: string;
version:string version: string;
name: string name: string;
description?: string description?: string;
homepage?: string homepage?: string;
license?: string license?: string;
author?: string author?: string;
repository?: string[], repository?: string[];
app_version?: string app_version?: string;
repository_ref: string repository_ref: string;
app_type: string app_type: string;
} }
@Entity() @Entity()
@ -78,6 +87,12 @@ export class Deployment {
@Column('simple-json') @Column('simple-json')
applicationRecordData!: ApplicationRecord; applicationRecordData!: ApplicationRecord;
@Column('varchar')
applicationDeploymentRequestId!: string;
@Column('simple-json')
applicationDeploymentRequestData!: ApplicationDeploymentRequest;
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
applicationDeploymentRecordId!: string | null; applicationDeploymentRecordId!: string | null;

View File

@ -27,8 +27,12 @@ export class Organization {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, { @OneToMany(
() => UserOrganization,
(userOrganization) => userOrganization.organization,
{
cascade: ['soft-remove'] cascade: ['soft-remove']
}) }
)
userOrganizations!: UserOrganization[]; userOrganizations!: UserOrganization[];
} }

View File

@ -15,15 +15,6 @@ import { Organization } from './Organization';
import { ProjectMember } from './ProjectMember'; import { ProjectMember } from './ProjectMember';
import { Deployment } from './Deployment'; import { Deployment } from './Deployment';
export interface ApplicationDeploymentRequest {
type: string
version: string
name: string
application: string
config: string,
meta: string
}
@Entity() @Entity()
export class Project { export class Project {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
@ -52,12 +43,6 @@ export class Project {
@Column('varchar', { length: 255, default: 'main' }) @Column('varchar', { length: 255, default: 'main' })
prodBranch!: string; prodBranch!: string;
@Column('varchar', { nullable: true })
applicationDeploymentRequestId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
@Column('text', { default: '' }) @Column('text', { default: '' })
description!: string; description!: string;
@ -91,7 +76,7 @@ export class Project {
@OneToMany(() => Deployment, (deployment) => deployment.project) @OneToMany(() => Deployment, (deployment) => deployment.project)
deployments!: Deployment[]; deployments!: Deployment[];
@OneToMany(() => ProjectMember, projectMember => projectMember.project, { @OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
cascade: ['soft-remove'] cascade: ['soft-remove']
}) })
projectMembers!: ProjectMember[]; projectMembers!: ProjectMember[];

View File

@ -15,7 +15,7 @@ import { User } from './User';
export enum Permission { export enum Permission {
View = 'View', View = 'View',
Edit = 'Edit' Edit = 'Edit',
} }
@Entity() @Entity()

View File

@ -34,13 +34,17 @@ export class User {
@CreateDateColumn() @CreateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@OneToMany(() => ProjectMember, projectMember => projectMember.project, { @OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
cascade: ['soft-remove'] cascade: ['soft-remove']
}) })
projectMembers!: ProjectMember[]; projectMembers!: ProjectMember[];
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, { @OneToMany(
() => UserOrganization,
(UserOrganization) => UserOrganization.member,
{
cascade: ['soft-remove'] cascade: ['soft-remove']
}) }
)
userOrganizations!: UserOrganization[]; userOrganizations!: UserOrganization[];
} }

View File

@ -19,7 +19,9 @@ const OAUTH_CLIENT_TYPE = 'oauth-app';
export const main = async (): Promise<void> => { export const main = async (): Promise<void> => {
// TODO: get config path using cli // TODO: get config path using cli
const { server, database, gitHub, registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const { server, database, gitHub, registryConfig } = await getConfig<Config>(
DEFAULT_CONFIG_FILE_PATH
);
const app = new OAuthApp({ const app = new OAuthApp({
clientType: OAUTH_CLIENT_TYPE, clientType: OAUTH_CLIENT_TYPE,
@ -31,9 +33,16 @@ export const main = async (): Promise<void> => {
await db.init(); await db.init();
const registry = new Registry(registryConfig); 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); const resolvers = await createResolvers(service);
await createAndStartServer(server, typeDefs, resolvers, service); await createAndStartServer(server, typeDefs, resolvers, service);

View File

@ -6,8 +6,11 @@ import { DateTime } from 'luxon';
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk'; import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
import { RegistryConfig } from './config'; import { RegistryConfig } from './config';
import { ApplicationDeploymentRequest } from './entity/Project'; import {
import { ApplicationRecord, Deployment } from './entity/Deployment'; ApplicationRecord,
Deployment,
ApplicationDeploymentRequest
} from './entity/Deployment';
import { AppDeploymentRecord, PackageJSON } from './types'; import { AppDeploymentRecord, PackageJSON } from './types';
const log = debug('snowball:registry'); const log = debug('snowball:registry');
@ -23,33 +26,52 @@ export class Registry {
constructor (registryConfig: RegistryConfig) { constructor (registryConfig: RegistryConfig) {
this.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 ({ async createApplicationRecord ({
appName,
packageJSON, packageJSON,
commitHash, commitHash,
appType, appType,
repoUrl repoUrl
}: { }: {
packageJSON: PackageJSON appName: string;
commitHash: string, packageJSON: PackageJSON;
appType: string, commitHash: string;
repoUrl: string appType: string;
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> { repoUrl: string;
assert(packageJSON.name, "name field doesn't exist in package.json"); }): Promise<{
applicationRecordId: string;
applicationRecordData: ApplicationRecord;
}> {
// Use laconic-sdk to publish record // 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 // Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
// Fetch previous records // Fetch previous records
const records = await this.registry.queryRecords({ const records = await this.registry.queryRecords(
{
type: APP_RECORD_TYPE, type: APP_RECORD_TYPE,
name: packageJSON.name name: packageJSON.name
}, true); },
true
);
// Get next version of record // Get next version of record
const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId); const bondRecords = records.filter(
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()); (record: any) => record.bondId === this.registryConfig.bondId
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch'); );
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'); assert(nextVersion, 'Application record version not valid');
@ -60,11 +82,16 @@ export class Registry {
repository_ref: commitHash, repository_ref: commitHash,
repository: [repoUrl], repository: [repoUrl],
app_type: appType, app_type: appType,
name: packageJSON.name, name: appName,
...(packageJSON.description && { description: packageJSON.description }), ...(packageJSON.description && { description: packageJSON.description }),
...(packageJSON.homepage && { homepage: packageJSON.homepage }), ...(packageJSON.homepage && { homepage: packageJSON.homepage }),
...(packageJSON.license && { license: packageJSON.license }), ...(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 }) ...(packageJSON.version && { app_version: packageJSON.version })
}; };
@ -81,26 +108,45 @@ export class Registry {
log('Application record data:', applicationRecord); log('Application record data:', applicationRecord);
// TODO: Discuss computation of CRN // 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}`); 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(
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee); { cid: result.data.id, crn },
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee); 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: { async createApplicationDeploymentRequest (data: {
appName: string, appName: string;
commitHash: string, packageJsonName: string;
repository: string, commitHash: string;
environmentVariables: { [key: string]: string } repository: string;
environmentVariables: { [key: string]: string };
}): Promise<{ }): Promise<{
applicationDeploymentRequestId: string, applicationDeploymentRequestId: string;
applicationDeploymentRequestData: ApplicationDeploymentRequest applicationDeploymentRequestData: ApplicationDeploymentRequest;
}> { }> {
const crn = this.getCrn(data.appName); const crn = this.getCrn(data.packageJsonName, data.appName);
const records = await this.registry.resolveNames([crn]); const records = await this.registry.resolveNames([crn]);
const applicationRecord = records[0]; const applicationRecord = records[0];
@ -124,7 +170,9 @@ export class Registry {
env: data.environmentVariables env: data.environmentVariables
}), }),
meta: JSON.stringify({ 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: data.repository,
repository_ref: data.commitHash repository_ref: data.commitHash
}) })
@ -142,31 +190,40 @@ export class Registry {
log(`Application deployment request record published: ${result.data.id}`); log(`Application deployment request record published: ${result.data.id}`);
log('Application deployment request data:', applicationDeploymentRequest); log('Application deployment request data:', applicationDeploymentRequest);
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest }; return {
applicationDeploymentRequestId: result.data.id,
applicationDeploymentRequestData: applicationDeploymentRequest
};
} }
/** /**
* Fetch ApplicationDeploymentRecords for deployments * Fetch ApplicationDeploymentRecords for deployments
*/ */
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> { async getDeploymentRecords (
deployments: Deployment[]
): Promise<AppDeploymentRecord[]> {
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments // Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute // TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
const records = await this.registry.queryRecords({ const records = await this.registry.queryRecords(
{
type: APP_DEPLOYMENT_RECORD_TYPE type: APP_DEPLOYMENT_RECORD_TYPE
}, true); },
true
);
// Filter records with ApplicationRecord ids // 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 { getCrn (packageJsonName: string, appName: string): string {
const [arg1, arg2] = packageJsonName.split('/'); const [arg1] = packageJsonName.split('/');
if (arg2) {
const authority = arg1.replace('@', ''); const authority = arg1.replace('@', '');
return `crn://${authority}/applications/${arg2}`;
}
return `crn://${arg1}/applications/${arg1}`; return `crn://${authority}/applications/${appName}`;
} }
} }

View File

@ -25,15 +25,25 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectById(projectId); return service.getProjectById(projectId);
}, },
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => { projectsInOrganization: async (
return service.getProjectsInOrganization(context.userId, organizationSlug); _: any,
{ organizationSlug }: { organizationSlug: string },
context: any
) => {
return service.getProjectsInOrganization(
context.userId,
organizationSlug
);
}, },
deployments: async (_: any, { projectId }: { projectId: string }) => { deployments: async (_: any, { projectId }: { projectId: string }) => {
return service.getDeploymentsByProjectId(projectId); return service.getDeploymentsByProjectId(projectId);
}, },
environmentVariables: async (_: any, { projectId }: { projectId: string }) => { environmentVariables: async (
_: any,
{ projectId }: { projectId: string }
) => {
return service.getEnvironmentVariablesByProjectId(projectId); return service.getEnvironmentVariablesByProjectId(projectId);
}, },
@ -41,32 +51,55 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectMembersByProjectId(projectId); 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); return service.searchProjects(context.userId, searchText);
}, },
domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere<Domain> }) => { domains: async (
_: any,
{
projectId,
filter
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
) => {
return service.getDomainsByProjectId(projectId, filter); return service.getDomainsByProjectId(projectId, filter);
} }
}, },
// TODO: Return error in GQL response // TODO: Return error in GQL response
Mutation: { Mutation: {
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => { removeProjectMember: async (
_: any,
{ projectMemberId }: { projectMemberId: string },
context: any
) => {
try { try {
return await service.removeProjectMember(context.userId, projectMemberId); return await service.removeProjectMember(
context.userId,
projectMemberId
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
updateProjectMember: async (_: any, { projectMemberId, data }: { updateProjectMember: async (
projectMemberId: string, _: any,
{
projectMemberId,
data
}: {
projectMemberId: string;
data: { data: {
permissions: Permission[] permissions: Permission[];
};
} }
}) => { ) => {
try { try {
return await service.updateProjectMember(projectMemberId, data); return await service.updateProjectMember(projectMemberId, data);
} catch (err) { } catch (err) {
@ -75,13 +108,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
addProjectMember: async (_: any, { projectId, data }: { addProjectMember: async (
projectId: string, _: any,
{
projectId,
data
}: {
projectId: string;
data: { data: {
email: string, email: string;
permissions: Permission[] permissions: Permission[];
};
} }
}) => { ) => {
try { try {
return Boolean(await service.addProjectMember(projectId, data)); return Boolean(await service.addProjectMember(projectId, data));
} catch (err) { } catch (err) {
@ -90,25 +129,51 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
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 { try {
return Boolean(await service.addEnvironmentVariables(projectId, data)); return Boolean(
await service.addEnvironmentVariables(projectId, data)
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => { updateEnvironmentVariable: async (
_: any,
{
environmentVariableId,
data
}: {
environmentVariableId: string;
data: DeepPartial<EnvironmentVariable>;
}
) => {
try { try {
return await service.updateEnvironmentVariable(environmentVariableId, data); return await service.updateEnvironmentVariable(
environmentVariableId,
data
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => { removeEnvironmentVariable: async (
_: any,
{ environmentVariableId }: { environmentVariableId: string }
) => {
try { try {
return await service.removeEnvironmentVariable(environmentVariableId); return await service.removeEnvironmentVariable(environmentVariableId);
} catch (err) { } catch (err) {
@ -117,25 +182,45 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => { updateDeploymentToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
context: any
) => {
try { try {
return Boolean(await service.updateDeploymentToProd(context.userId, deploymentId)); return Boolean(
await service.updateDeploymentToProd(context.userId, deploymentId)
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
addProject: async (_: any, { organizationSlug, data }: { organizationSlug: string, data: DeepPartial<Project> }, context: any) => { addProject: async (
_: any,
{
organizationSlug,
data
}: { organizationSlug: string; data: DeepPartial<Project> },
context: any
) => {
try { try {
return await service.addProject(context.userId, organizationSlug, data); return await service.addProject(
context.userId,
organizationSlug,
data
);
} catch (err) { } catch (err) {
log(err); log(err);
throw err; throw err;
} }
}, },
updateProject: async (_: any, { projectId, data }: { projectId: string, data: DeepPartial<Project> }) => { updateProject: async (
_: any,
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
) => {
try { try {
return await service.updateProject(projectId, data); return await service.updateProject(projectId, data);
} catch (err) { } catch (err) {
@ -144,9 +229,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => { redeployToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
context: any
) => {
try { try {
return Boolean(await service.redeployToProd(context.userId, deploymentId)); return Boolean(
await service.redeployToProd(context.userId, deploymentId)
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@ -157,7 +248,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
try { try {
return await service.deleteProject(projectId); return await service.deleteProject(projectId);
} catch (err) { } catch (err) {
log(err); return false; log(err);
return false;
} }
}, },
@ -170,7 +262,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => { rollbackDeployment: async (
_: any,
{
projectId,
deploymentId
}: { deploymentId: string; projectId: string }
) => {
try { try {
return await service.rollbackDeployment(projectId, deploymentId); return await service.rollbackDeployment(projectId, deploymentId);
} catch (err) { } catch (err) {
@ -179,7 +277,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
addDomain: async (_: any, { projectId, data }: { projectId: string, data: { name: string } }) => { addDomain: async (
_: any,
{ projectId, data }: { projectId: string; data: { name: string } }
) => {
try { try {
return Boolean(await service.addDomain(projectId, data)); return Boolean(await service.addDomain(projectId, data));
} catch (err) { } catch (err) {
@ -188,7 +289,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
updateDomain: async (_: any, { domainId, data }: { domainId: string, data: DeepPartial<Domain>}) => { updateDomain: async (
_: any,
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
) => {
try { try {
return await service.updateDomain(domainId, data); return await service.updateDomain(domainId, data);
} catch (err) { } catch (err) {
@ -197,7 +301,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => { authenticateGitHub: async (
_: any,
{ code }: { code: string },
context: any
) => {
try { try {
return await service.authenticateGitHub(code, context.userId); return await service.authenticateGitHub(code, context.userId);
} catch (err) { } catch (err) {
@ -208,7 +316,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
unauthenticateGitHub: async (_: any, __: object, context: any) => { unauthenticateGitHub: async (_: any, __: object, context: any) => {
try { try {
return service.unauthenticateGitHub(context.userId, { gitHubToken: null }); return service.unauthenticateGitHub(context.userId, {
gitHubToken: null
});
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;

View File

@ -188,10 +188,19 @@ type Query {
type Mutation { type Mutation {
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean! addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean! updateProjectMember(
projectMemberId: String!
data: UpdateProjectMemberInput
): Boolean!
removeProjectMember(projectMemberId: String!): Boolean! removeProjectMember(projectMemberId: String!): Boolean!
addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean! addEnvironmentVariables(
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean! projectId: String!
data: [AddEnvironmentVariableInput!]
): Boolean!
updateEnvironmentVariable(
environmentVariableId: String!
data: UpdateEnvironmentVariableInput!
): Boolean!
removeEnvironmentVariable(environmentVariableId: String!): Boolean! removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateDeploymentToProd(deploymentId: String!): Boolean! updateDeploymentToProd(deploymentId: String!): Boolean!
addProject(organizationSlug: String!, data: AddProjectInput): Project! addProject(organizationSlug: String!, data: AddProjectInput): Project!

View File

@ -15,15 +15,15 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
import { User } from './entity/User'; import { User } from './entity/User';
import { Registry } from './registry'; import { Registry } from './registry';
import { GitHubConfig, RegistryConfig } from './config'; import { GitHubConfig, RegistryConfig } from './config';
import { AppDeploymentRecord, GitPushEventPayload } from './types'; import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
const log = debug('snowball:service'); const log = debug('snowball:service');
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository'; const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
interface Config { interface Config {
gitHubConfig: GitHubConfig gitHubConfig: GitHubConfig;
registryConfig: RegistryConfig registryConfig: RegistryConfig;
} }
export class Service { export class Service {
@ -71,7 +71,9 @@ export class Service {
}); });
if (deployments.length) { 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 // Fetch ApplicationDeploymentRecord for deployments
const records = await this.registry.getDeploymentRecords(deployments); const records = await this.registry.getDeploymentRecords(deployments);
@ -91,10 +93,12 @@ export class Service {
/** /**
* Update deployments with ApplicationDeploymentRecord data * Update deployments with ApplicationDeploymentRecord data
*/ */
async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise<void> { async updateDeploymentsWithRecordData (
records: AppDeploymentRecord[]
): Promise<void> {
// Get deployments for ApplicationDeploymentRecords // Get deployments for ApplicationDeploymentRecords
const deployments = await this.db.getDeployments({ const deployments = await this.db.getDeployments({
where: records.map(record => ({ where: records.map((record) => ({
applicationRecordId: record.attributes.application applicationRecordId: record.attributes.application
})), })),
order: { order: {
@ -103,38 +107,46 @@ export class Service {
}); });
// Get project IDs of deployments that are in production environment // Get project IDs of deployments that are in production environment
const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set<string> => { const productionDeploymentProjectIds = deployments.reduce(
(acc, deployment): Set<string> => {
if (deployment.environment === Environment.Production) { if (deployment.environment === Environment.Production) {
acc.add(deployment.projectId); acc.add(deployment.projectId);
} }
return acc; return acc;
}, new Set<string>()); },
new Set<string>()
);
// Set old deployments isCurrent to false // 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) => { const recordToDeploymentsMap = deployments.reduce(
(acc: { [key: string]: Deployment }, deployment) => {
acc[deployment.applicationRecordId] = deployment; acc[deployment.applicationRecordId] = deployment;
return acc; return acc;
}, {}); },
{}
);
// Update deployment data for ApplicationDeploymentRecords // Update deployment data for ApplicationDeploymentRecords
const deploymentUpdatePromises = records.map(async (record) => { const deploymentUpdatePromises = records.map(async (record) => {
const deployment = recordToDeploymentsMap[record.attributes.application]; const deployment = recordToDeploymentsMap[record.attributes.application];
await this.db.updateDeploymentById( await this.db.updateDeploymentById(deployment.id, {
deployment.id,
{
applicationDeploymentRecordId: record.id, applicationDeploymentRecordId: record.id,
applicationDeploymentRecordData: record.attributes, applicationDeploymentRecordData: record.attributes,
url: record.attributes.url, url: record.attributes.url,
status: DeploymentStatus.Ready, status: DeploymentStatus.Ready,
isCurrent: deployment.environment === Environment.Production 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); await Promise.all(deploymentUpdatePromises);
@ -150,7 +162,10 @@ export class Service {
async getOctokit (userId: string): Promise<Octokit> { async getOctokit (userId: string): Promise<Octokit> {
const user = await this.db.getUser({ where: { id: userId } }); 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 }); return new Octokit({ auth: user.gitHubToken });
} }
@ -165,8 +180,14 @@ export class Service {
return dbProject; return dbProject;
} }
async getProjectsInOrganization (userId:string, organizationSlug: string): Promise<Project[]> { async getProjectsInOrganization (
const dbProjects = await this.db.getProjectsInOrganization(userId, organizationSlug); userId: string,
organizationSlug: string
): Promise<Project[]> {
const dbProjects = await this.db.getProjectsInOrganization(
userId,
organizationSlug
);
return dbProjects; return dbProjects;
} }
@ -175,35 +196,52 @@ export class Service {
return dbDeployments; return dbDeployments;
} }
async getEnvironmentVariablesByProjectId (projectId: string): Promise<EnvironmentVariable[]> { async getEnvironmentVariablesByProjectId (
const dbEnvironmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId); projectId: string
): Promise<EnvironmentVariable[]> {
const dbEnvironmentVariables =
await this.db.getEnvironmentVariablesByProjectId(projectId);
return dbEnvironmentVariables; return dbEnvironmentVariables;
} }
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> { async getProjectMembersByProjectId (
const dbProjectMembers = await this.db.getProjectMembersByProjectId(projectId); projectId: string
): Promise<ProjectMember[]> {
const dbProjectMembers =
await this.db.getProjectMembersByProjectId(projectId);
return dbProjectMembers; return dbProjectMembers;
} }
async searchProjects (userId: string, searchText: string): Promise<Project[]> { async searchProjects (userId: string, searchText: string): Promise<Project[]> {
const dbProjects = await this.db.getProjectsBySearchText(userId, searchText); const dbProjects = await this.db.getProjectsBySearchText(
userId,
searchText
);
return dbProjects; return dbProjects;
} }
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> { async getDomainsByProjectId (
projectId: string,
filter?: FindOptionsWhere<Domain>
): Promise<Domain[]> {
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter); const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
return dbDomains; return dbDomains;
} }
async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise<boolean> { async updateProjectMember (
projectMemberId: string,
data: { permissions: Permission[] }
): Promise<boolean> {
return this.db.updateProjectMemberById(projectMemberId, data); return this.db.updateProjectMemberById(projectMemberId, data);
} }
async addProjectMember (projectId: string, async addProjectMember (
projectId: string,
data: { data: {
email: string, email: string;
permissions: Permission[] permissions: Permission[];
}): Promise<ProjectMember> { }
): Promise<ProjectMember> {
// TODO: Send invitation // TODO: Send invitation
let user = await this.db.getUser({ let user = await this.db.getUser({
where: { where: {
@ -231,7 +269,10 @@ export class Service {
return newProjectMember; return newProjectMember;
} }
async removeProjectMember (userId: string, projectMemberId: string): Promise<boolean> { async removeProjectMember (
userId: string,
projectMemberId: string
): Promise<boolean> {
const member = await this.db.getProjectMemberById(projectMemberId); const member = await this.db.getProjectMemberById(projectMemberId);
if (String(member.member.id) === userId) { 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<EnvironmentVariable[]> { async addEnvironmentVariables (
const formattedEnvironmentVariables = data.map((environmentVariable) => { projectId: string,
data: { environments: string[]; key: string; value: string }[]
): Promise<EnvironmentVariable[]> {
const formattedEnvironmentVariables = data
.map((environmentVariable) => {
return environmentVariable.environments.map((environment) => { return environmentVariable.environments.map((environment) => {
return ({ return {
key: environmentVariable.key, key: environmentVariable.key,
value: environmentVariable.value, value: environmentVariable.value,
environment: environment as Environment, environment: environment as Environment,
project: Object.assign(new Project(), { project: Object.assign(new Project(), {
id: projectId id: projectId
}) })
};
}); });
}); })
}).flat(); .flat();
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables); const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
formattedEnvironmentVariables
);
return savedEnvironmentVariables; return savedEnvironmentVariables;
} }
async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial<EnvironmentVariable>): Promise<boolean> { async updateEnvironmentVariable (
environmentVariableId: string,
data: DeepPartial<EnvironmentVariable>
): Promise<boolean> {
return this.db.updateEnvironmentVariable(environmentVariableId, data); return this.db.updateEnvironmentVariable(environmentVariableId, data);
} }
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> { async removeEnvironmentVariable (
environmentVariableId: string
): Promise<boolean> {
return this.db.deleteEnvironmentVariable(environmentVariableId); return this.db.deleteEnvironmentVariable(environmentVariableId);
} }
async updateDeploymentToProd (userId: string, deploymentId: string): Promise<Deployment> { async updateDeploymentToProd (
userId: string,
deploymentId: string
): Promise<Deployment> {
const oldDeployment = await this.db.getDeployment({ const oldDeployment = await this.db.getDeployment({
where: { id: deploymentId }, where: { id: deploymentId },
relations: { relations: {
@ -286,13 +342,14 @@ export class Service {
throw new Error('Deployment does not exist'); 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 octokit = await this.getOctokit(userId);
const newDeployment = await this.createDeployment(userId, const newDeployment = await this.createDeployment(userId, octokit, {
octokit,
{
project: oldDeployment.project, project: oldDeployment.project,
branch: oldDeployment.branch, branch: oldDeployment.branch,
environment: Environment.Production, environment: Environment.Production,
@ -311,7 +368,9 @@ export class Service {
recordData: { repoUrl?: string } = {} recordData: { repoUrl?: string } = {}
): Promise<Deployment> { ): Promise<Deployment> {
assert(data.project?.repository, 'Project repository not found'); 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 [owner, repo] = data.project.repository.split('/');
const { data: packageJSONData } = await octokit.rest.repos.getContent({ const { data: packageJSONData } = await octokit.rest.repos.getContent({
@ -326,28 +385,61 @@ export class Service {
} }
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file'); 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) { 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; recordData.repoUrl = repoDetails.html_url;
} }
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record) // TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({ const { applicationRecordId, applicationRecordData } =
await this.registry.createApplicationRecord({
appName: repo,
packageJSON, packageJSON,
appType: data.project!.template!, appType: data.project!.template!,
commitHash: data.commitHash!, commitHash: data.commitHash!,
repoUrl: recordData.repoUrl 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 // Update previous deployment with prod branch domain
// TODO: Fix unique constraint error for domain // TODO: Fix unique constraint error for domain
await this.db.updateDeployment({ await this.db.updateDeployment(
{
domainId: data.domain?.id domainId: data.domain?.id
}, { },
{
domain: null domain: null
}); }
);
const newDeployment = await this.db.addDeployment({ const newDeployment = await this.db.addDeployment({
project: data.project, project: data.project,
@ -358,17 +450,25 @@ export class Service {
status: DeploymentStatus.Building, status: DeploymentStatus.Building,
applicationRecordId, applicationRecordId,
applicationRecordData, applicationRecordData,
applicationDeploymentRequestId,
applicationDeploymentRequestData,
domain: data.domain, domain: data.domain,
createdBy: Object.assign(new User(), { createdBy: Object.assign(new User(), {
id: userId id: userId
}) })
}); });
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`); log(
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`
);
return newDeployment; return newDeployment;
} }
async addProject (userId: string, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> { async addProject (
userId: string,
organizationSlug: string,
data: DeepPartial<Project>
): Promise<Project | undefined> {
const organization = await this.db.getOrganization({ const organization = await this.db.getOrganization({
where: { where: {
slug: organizationSlug slug: organizationSlug
@ -383,7 +483,9 @@ export class Service {
const octokit = await this.getOctokit(userId); const octokit = await this.getOctokit(userId);
const [owner, repo] = project.repository.split('/'); const [owner, repo] = project.repository.split('/');
const { data: [latestCommit] } = await octokit.rest.repos.listCommits({ const {
data: [latestCommit]
} = await octokit.rest.repos.listCommits({
owner, owner,
repo, repo,
sha: project.prodBranch, sha: project.prodBranch,
@ -393,7 +495,8 @@ export class Service {
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo }); const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
// Create deployment with prod branch and latest commit // Create deployment with prod branch and latest commit
const newDeployment = await this.createDeployment(userId, await this.createDeployment(
userId,
octokit, octokit,
{ {
project, 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); await this.createRepoHook(octokit, project);
return project; return project;
@ -441,7 +523,10 @@ export class Service {
owner, owner,
repo, repo,
config: { 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' content_type: 'json'
}, },
events: ['push'] events: ['push']
@ -449,9 +534,13 @@ export class Service {
} catch (err) { } catch (err) {
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes // https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
if ( if (
!(err instanceof RequestError && !(
err instanceof RequestError &&
err.status === 422 && err.status === 422 &&
(err.response?.data as any).errors.some((err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR)) (err.response?.data as any).errors.some(
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
)
)
) { ) {
throw err; throw err;
} }
@ -463,7 +552,9 @@ export class Service {
async handleGitHubPush (data: GitPushEventPayload): Promise<void> { async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
const { repository, ref, head_commit: headCommit } = data; const { repository, ref, head_commit: headCommit } = data;
log(`Handling GitHub push event from repository: ${repository.full_name}`); 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) { if (!projects.length) {
log(`No projects found for repository ${repository.full_name}`); log(`No projects found for repository ${repository.full_name}`);
@ -475,15 +566,18 @@ export class Service {
for await (const project of projects) { for await (const project of projects) {
const octokit = await this.getOctokit(project.ownerId); 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 // Create deployment with branch and latest commit in GitHub data
await this.createDeployment(project.ownerId, await this.createDeployment(project.ownerId, octokit, {
octokit,
{
project, project,
branch, branch,
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview, environment:
project.prodBranch === branch
? Environment.Production
: Environment.Preview,
domain, domain,
commitHash: headCommit.id, commitHash: headCommit.id,
commitMessage: headCommit.message commitMessage: headCommit.message
@ -491,7 +585,10 @@ export class Service {
} }
} }
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> { async updateProject (
projectId: string,
data: DeepPartial<Project>
): Promise<boolean> {
return this.db.updateProjectById(projectId, data); return this.db.updateProjectById(projectId, data);
} }
@ -508,13 +605,18 @@ export class Service {
}); });
if (domainsRedirectedFrom.length > 0) { 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); return this.db.deleteDomainById(domainId);
} }
async redeployToProd (userId: string, deploymentId: string): Promise<Deployment> { async redeployToProd (
userId: string,
deploymentId: string
): Promise<Deployment> {
const oldDeployment = await this.db.getDeployment({ const oldDeployment = await this.db.getDeployment({
relations: { relations: {
project: true, project: true,
@ -532,9 +634,7 @@ export class Service {
const octokit = await this.getOctokit(userId); const octokit = await this.getOctokit(userId);
const newDeployment = await this.createDeployment(userId, const newDeployment = await this.createDeployment(userId, octokit, {
octokit,
{
project: oldDeployment.project, project: oldDeployment.project,
// TODO: Put isCurrent field in project // TODO: Put isCurrent field in project
branch: oldDeployment.branch, branch: oldDeployment.branch,
@ -547,7 +647,10 @@ export class Service {
return newDeployment; return newDeployment;
} }
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> { async rollbackDeployment (
projectId: string,
deploymentId: string
): Promise<boolean> {
// TODO: Implement transactions // TODO: Implement transactions
const oldCurrentDeployment = await this.db.getDeployment({ const oldCurrentDeployment = await this.db.getDeployment({
relations: { relations: {
@ -565,16 +668,25 @@ export class Service {
throw new Error('Current deployment doesnot exist'); 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; return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
} }
async addDomain (projectId: string, data: { name: string }): Promise<{ async addDomain (
primaryDomain: Domain, projectId: string,
redirectedDomain: Domain data: { name: string }
): Promise<{
primaryDomain: Domain;
redirectedDomain: Domain;
}> { }> {
const currentProject = await this.db.getProjectById(projectId); const currentProject = await this.db.getProjectById(projectId);
@ -599,12 +711,20 @@ export class Service {
redirectTo: savedPrimaryDomain 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<Domain>): Promise<boolean> { async updateDomain (
domainId: string,
data: DeepPartial<Domain>
): Promise<boolean> {
const domain = await this.db.getDomain({ const domain = await this.db.getDomain({
where: { where: {
id: domainId id: domainId
@ -645,7 +765,9 @@ export class Service {
} }
if (redirectedDomain.redirectToId) { 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; newDomain.redirectTo = redirectedDomain;
@ -656,8 +778,13 @@ export class Service {
return updateResult; return updateResult;
} }
async authenticateGitHub (code:string, userId: string): Promise<{token: string}> { async authenticateGitHub (
const { authentication: { token } } = await this.oauthApp.createToken({ code: string,
userId: string
): Promise<{ token: string }> {
const {
authentication: { token }
} = await this.oauthApp.createToken({
code code
}); });
@ -666,7 +793,10 @@ export class Service {
return { token }; return { token };
} }
async unauthenticateGitHub (userId: string, data: DeepPartial<User>): Promise<boolean> { async unauthenticateGitHub (
userId: string,
data: DeepPartial<User>
): Promise<boolean> {
return this.db.updateUser(userId, data); return this.db.updateUser(userId, data);
} }
} }

View File

@ -1,6 +1,6 @@
export interface PackageJSON { export interface PackageJSON {
name?: string; name: string;
version?: string; version: string;
author?: string; author?: string;
description?: string; description?: string;
homepage?: string; homepage?: string;
@ -47,5 +47,5 @@ interface RegistryRecord {
} }
export interface AppDeploymentRecord extends RegistryRecord { export interface AppDeploymentRecord extends RegistryRecord {
attributes: AppDeploymentRecordAttributes attributes: AppDeploymentRecordAttributes;
} }

View File

@ -2,6 +2,7 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import toml from 'toml'; import toml from 'toml';
import debug from 'debug'; import debug from 'debug';
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
const log = debug('snowball:utils'); const log = debug('snowball:utils');
@ -19,3 +20,49 @@ export const getConfig = async <ConfigType>(
return config; return config;
}; };
export const checkFileExists = async (filePath: string): Promise<boolean> => {
try {
await fs.access(filePath, fs.constants.F_OK);
return true;
} catch (err) {
log(err);
return false;
}
};
export const getEntities = async (filePath: string): Promise<any> => {
const entitiesData = await fs.readFile(filePath, 'utf-8');
const entities = JSON.parse(entitiesData);
return entities;
};
export const loadAndSaveData = async <Entity extends ObjectLiteral>(
entityType: EntityTarget<Entity>,
dataSource: DataSource,
entities: any,
relations?: any | undefined
): Promise<Entity[]> => {
const entityRepository = dataSource.getRepository(entityType);
const savedEntity: Entity[] = [];
for (const entityData of entities) {
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
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;
};

View File

@ -18,4 +18,4 @@ const main = async () => {
deleteFile(config.database.dbPath); deleteFile(config.database.dbPath);
}; };
main().catch(err => log(err)); main().catch((err) => log(err));

View File

@ -9,6 +9,8 @@
"isCurrent": true, "isCurrent": true,
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main", "branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -24,6 +26,8 @@
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -39,6 +43,8 @@
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -54,6 +60,8 @@
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi", "applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "prod", "branch": "prod",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -69,6 +77,8 @@
"isCurrent": true, "isCurrent": true,
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main", "branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -84,6 +94,8 @@
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -99,6 +111,8 @@
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi", "applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -114,6 +128,8 @@
"isCurrent": true, "isCurrent": true,
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "main", "branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -129,6 +145,8 @@
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -144,6 +162,8 @@
"isCurrent": false, "isCurrent": false,
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",
@ -159,6 +179,8 @@
"isCurrent": true, "isCurrent": true,
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"applicationRecordData": {}, "applicationRecordData": {},
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"branch": "test", "branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added", "commitMessage": "subscription added",

View File

@ -2,77 +2,55 @@
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 0, "projectIndex": 0,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 0, "projectIndex": 0,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 1, "projectIndex": 1,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 0, "memberIndex": 0,
"projectIndex": 2, "projectIndex": 2,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 2, "projectIndex": 2,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 0, "memberIndex": 0,
"projectIndex": 3, "projectIndex": 3,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 3, "projectIndex": 3,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 4, "projectIndex": 4,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 4, "projectIndex": 4,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
} }
] ]

View File

@ -10,8 +10,6 @@
"framework": "test", "framework": "test",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "testProject.snowball.xyz" "subDomain": "testProject.snowball.xyz"
}, },
{ {
@ -25,8 +23,6 @@
"framework": "test-2", "framework": "test-2",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "testProject-2.snowball.xyz" "subDomain": "testProject-2.snowball.xyz"
}, },
{ {
@ -40,8 +36,6 @@
"framework": "test-3", "framework": "test-3",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "iglootools.snowball.xyz" "subDomain": "iglootools.snowball.xyz"
}, },
{ {
@ -55,8 +49,6 @@
"framework": "test-4", "framework": "test-4",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "iglootools-2.snowball.xyz" "subDomain": "iglootools-2.snowball.xyz"
}, },
{ {
@ -70,8 +62,6 @@
"framework": "test-5", "framework": "test-5",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "snowball-2.snowball.xyz" "subDomain": "snowball-2.snowball.xyz"
} }
] ]

View File

@ -1,5 +1,4 @@
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm'; import { DataSource } from 'typeorm';
import * as fs from 'fs/promises';
import debug from 'debug'; import debug from 'debug';
import path from 'path'; import path from 'path';
@ -11,7 +10,12 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable';
import { Domain } from '../src/entity/Domain'; import { Domain } from '../src/entity/Domain';
import { ProjectMember } from '../src/entity/ProjectMember'; import { ProjectMember } from '../src/entity/ProjectMember';
import { Deployment } from '../src/entity/Deployment'; 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 { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants'; 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 ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json'; const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, 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<Entity>);
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 generateTestData = async (dataSource: DataSource) => {
const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH)); const userEntities = await getEntities(
const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH)); 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 = { const projectRelations = {
owner: savedUsers, owner: savedUsers,
organization: savedOrgs 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); const domainRepository = dataSource.getRepository(Domain);
@ -71,14 +67,30 @@ const generateTestData = async (dataSource: DataSource) => {
project: savedProjects 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 = { const domainRedirectedRelations = {
project: savedProjects, project: savedProjects,
redirectTo: savedPrimaryDomains 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(); const savedDomains = await domainRepository.find();
@ -87,14 +99,30 @@ const generateTestData = async (dataSource: DataSource) => {
organization: savedOrgs 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 = { const projectMemberRelations = {
member: savedUsers, member: savedUsers,
project: savedProjects 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 = { const deploymentRelations = {
project: savedProjects, project: savedProjects,
@ -102,23 +130,29 @@ const generateTestData = async (dataSource: DataSource) => {
createdBy: savedUsers 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 = { const environmentVariableRelations = {
project: savedProjects project: savedProjects
}; };
await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations); const environmentVariablesEntities = await getEntities(
}; path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
);
const checkFileExists = async (filePath: string) => { await loadAndSaveData(
try { EnvironmentVariable,
await fs.access(filePath, fs.constants.F_OK); dataSource,
return true; environmentVariablesEntities,
} catch (err) { environmentVariableRelations
log(err); );
return false;
}
}; };
const main = async () => { const main = async () => {

View File

@ -17,16 +17,32 @@ const AUTHORITY_NAMES = ['snowballtools', 'cerc-io'];
async function main () { async function main () {
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const { registryConfig } = await getConfig<Config>(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); const bondId = await registry.getNextBondId(registryConfig.privateKey);
log('bondId:', bondId); 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) { 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); 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}`); log(`Bond ${bondId} set for authority ${name}`);
} }
} }

View File

@ -12,9 +12,15 @@ import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
const log = debug('snowball:publish-deploy-records'); const log = debug('snowball:publish-deploy-records');
async function main () { async function main () {
const { registryConfig, database } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const { registryConfig, database } = await getConfig<Config>(
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({ const dataSource = new DataSource({
type: 'better-sqlite3', type: 'better-sqlite3',
@ -53,7 +59,7 @@ async function main () {
so: '66fcfa49a1664d4cb4ce4f72c1c0e151' so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
}), }),
request: deployment.project.applicationDeploymentRequestId, request: deployment.applicationDeploymentRequestId,
url url
}; };

View File

@ -16,6 +16,7 @@ const DEFAULT_FILTER_VALUE: FilterValue = {
searchedBranch: '', searchedBranch: '',
status: StatusOptions.ALL_STATUS, status: StatusOptions.ALL_STATUS,
}; };
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
const DeploymentsTabPanel = () => { const DeploymentsTabPanel = () => {
const client = useGQLClient(); const client = useGQLClient();
@ -26,22 +27,30 @@ const DeploymentsTabPanel = () => {
const [deployments, setDeployments] = useState<Deployment[]>([]); const [deployments, setDeployments] = useState<Deployment[]>([]);
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]); const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
const fetchDeployments = async () => { const fetchDeployments = useCallback(async () => {
const { deployments } = await client.getDeployments(project.id); const { deployments } = await client.getDeployments(project.id);
setDeployments(deployments); setDeployments(deployments);
}; }, [client, project]);
const fetchProductionBranchDomains = useCallback(async () => { const fetchProductionBranchDomains = useCallback(async () => {
const { domains } = await client.getDomains(project.id, { const { domains } = await client.getDomains(project.id, {
branch: project.prodBranch, branch: project.prodBranch,
}); });
setProdBranchDomains(domains); setProdBranchDomains(domains);
}, []); }, [client, project]);
useEffect(() => { useEffect(() => {
fetchDeployments();
fetchProductionBranchDomains(); fetchProductionBranchDomains();
}, []); fetchDeployments();
const interval = setInterval(() => {
fetchDeployments();
}, FETCH_DEPLOYMENTS_INTERVAL);
return () => {
clearInterval(interval);
};
}, [fetchDeployments, fetchProductionBranchDomains]);
const currentDeployment = useMemo(() => { const currentDeployment = useMemo(() => {
return deployments.find((deployment) => { return deployments.find((deployment) => {