Compare commits

..
598 changed files with 11683 additions and 34618 deletions
-2
View File
@@ -19,7 +19,5 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: yarn - run: yarn
- name: Build libs
run: yarn workspace gql-client run build
- name: Linter check - name: Linter check
run: yarn lint run: yarn lint
@@ -1,39 +0,0 @@
name: Test webapp deployment
on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
jobs:
test_app_deployment:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn
- name: Test webapp deployment
run: ./packages/deployer/test/test-webapp-deployment-undeployment.sh
- name: Notify Vulcanize Slack on CI failure
if: ${{ always() && github.ref_name == 'main' }}
uses: ravsamhq/notify-slack-action@v2
with:
status: ${{ job.status }}
notify_when: 'failure'
env:
SLACK_WEBHOOK_URL: ${{ secrets.VULCANIZE_SLACK_CI_ALERTS_WEBHOOK }}
- name: Notify DeepStack Slack on CI failure
if: ${{ always() && github.ref_name == 'main' }}
uses: ravsamhq/notify-slack-action@v2
with:
status: ${{ job.status }}
notify_when: 'failure'
env:
SLACK_WEBHOOK_URL: ${{ secrets.DEEPSTACK_SLACK_CI_ALERTS_WEBHOOK }}
-8
View File
@@ -1,9 +1 @@
node_modules/ node_modules/
yarn-error.log
.yarnrc.yml
.yarn/
.yarnrc
packages/backend/environments/local.toml
packages/backend/dev/
packages/frontend/dist/
-1
View File
@@ -1 +0,0 @@
v20.12.1
+1 -2
View File
@@ -1,7 +1,6 @@
{ {
// IntelliSense for taiwind variants // IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [ "tailwindCSS.experimental.classRegex": [
"tv\\('([^)]*)\\')", ["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
] ]
} }
+122 -192
View File
@@ -1,15 +1,14 @@
# snowballtools-base # snowballtools
## Setup ## Setup
- Clone the `snowballtools-base` repo - Clone the `snowballtools` repo
```bash ```bash
git clone git@github.com:snowball-tools/snowballtools-base.git git clone git@github.com:snowball-tools/snowballtools-base.git
cd snowballtools-base
``` ```
- Install dependencies in root - In root of the repo, install depedencies
```bash ```bash
yarn yarn
@@ -21,200 +20,131 @@
yarn build --ignore frontend yarn build --ignore frontend
``` ```
Let us assume the following domains for backend and frontend ## Backend
- Backend server: `api.snowballtools.com`
- Frontend app: `dashboard.snowballtools.com`
- Configuration Files - Change directory to `packages/backend`
- For Backend:
```bash
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
```
- Production
- Update the following in backend [config file](packages/backend/environments/local.toml)
```toml
[server]
...
[server.session]
# Secret should be changed to a different random string
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
# Set URL of the frontend app
appOriginUrl = "https://dashboard.snowballtools.com"
# Set to true for session cookies to work behind proxy
trustProxy = true
# Set empty domain when using secure connection
domain = ""
```
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
- Homepage URL: `https://dashboard.snowballtools.com`
- Authorization callback URL: `https://dashboard.snowballtools.com/organization/projects/create`
- Generate a new client secret after app is created
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
[gitHub]
webhookUrl = "https://api.snowballtools.com"
```
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
```toml
[registryConfig]
fetchDeploymentRecordDelay = 5000
# Use actual port for REST endpoint
restEndpoint = "http://api.laconic.com:1317"
# Use actual port for GQL endpoint
gqlEndpoint = "http://api.laconic.com:9473/api"
# Set private key of account to be used in Laconicd
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
# Set Bond ID to be used for publishing records
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
chainId = "laconic_9000-1"
# Set authority that is existing in the chain
authority = "laconic"
[registryConfig.fee]
amount = "200000"
denom = "aphoton"
gas = "750000"
```
- Development
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
- Homepage URL: `http://localhost:3000`
- Authorization callback URL: `http://localhost:3000/organization/projects/create`
- Generate a new client secret after app is created
- Setup Laconicd
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
```bash
laconic-so deployment --dir laconic-loaded-deployment exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
```
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
- Run the script to create bond, reserve the authority and set authority bond
```bash
yarn test:registry:init
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
```
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
- Setup ngrok for GitHub webhooks
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
- Start ngrok and point to backend server endpoint
```bash
ngrok http http://localhost:8000
```
- Look for the forwarding URL in ngrok
```bash
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
```
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
[gitHub]
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
```
- For Frontend:
```bash
cp packages/frontend/.env.example packages/frontend/.env
```
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
```env
VITE_GITHUB_CLIENT_ID = <CLIENT_ID>
```
- Set `VITE_GITHUB_PWA_TEMPLATE_REPO` and `VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
```env
VITE_GITHUB_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo
```
- Production
- Set the following values in [.env](packages/frontend/.env) file
```env
VITE_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint
```
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
- Create a project and add information to use wallet connect SDK
- Add project name and select project type as `App`
- Set project home page URL to `https://dashboard.snowballtools.com`
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
```env
VITE_WALLET_CONNECT_ID = <PROJECT_ID>
```
- Build the React application
```bash
yarn build
```
- Use a web server for hosting static built files
```bash
python3 -m http.server -d build 3000
```
- Development
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
```env
VITE_SERVER_URL = 'http://localhost:8000'
```
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
- Create a project and add information to use wallet connect SDK
- Add project name and select project type as `App`
- Project home page URL is not required to be set
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
```env
VITE_WALLET_CONNECT_ID = <Project_ID>
```
- The React application will be running in `http://localhost:3000/`
## Development
- Start the backend server
```bash ```bash
yarn start:backend cd packages/backend
``` ```
- Start the frontend - Load fixtures in database
```bash
yarn test:db:load:fixtures
```
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- Client ID and secret will be available after creating Github OAuth app
- https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app
- In "Homepage URL", type `http://localhost:3000`
- In "Authorization callback URL", type `http://localhost:3000/organization/projects/create`
- Generate a new client secret after app is created
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
```bash
laconic-so --stack fixturenet-laconic-loaded deploy exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
```
- Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml)
```bash
# For registryConfig.restEndpoint
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
# 0.0.0.0:32777
# For registryConfig.gqlEndpoint
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
# 0.0.0.0:32771
```
- Run the script to create bond, reserve the authority and set authority bond
```bash
yarn test:registry:init
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
```
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
- Setup ngrok for GitHub webhooks
- https://ngrok.com/docs/getting-started/
- Start ngrok and point to backend server endpoint
```bash
ngrok http http://localhost:8000
```
- Look for the forwarding URL in ngrok
```
...
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
...
```
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
...
[gitHub]
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
...
```
- Start the server in `packages/backend`
```bash ```bash
yarn start yarn start
``` ```
## Frontend
- Change directory to `packages/frontend` in a new terminal
```bash
cd packages/frontend
```
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
```env
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
```
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
```env
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
```
- Set `REACT_APP_GITHUB_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
```env
REACT_APP_GITHUB_TEMPLATE_REPO = cerc-io/test-progressive-web-app
```
### Development
- Start the React application
```bash
yarn start
```
- The React application will be running in `http://localhost:3000/`
### Production
- Build the React application
```bash
yarn build
```
- Use a web server for hosting static built files
```bash
python3 -m http.server -d build 3000
```
-34
View File
@@ -1,34 +0,0 @@
#!/bin/bash
PKG_DIR="./packages/frontend"
OUTPUT_DIR="${PKG_DIR}/dist"
DEST_DIR=${1:-/data}
if [[ -d "$DEST_DIR" ]]; then
echo "${DEST_DIR} already exists." 1>&2
exit 1
fi
cat > $PKG_DIR/.env <<EOF
VITE_SERVER_URL = 'LACONIC_HOSTED_CONFIG_server_url'
VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid'
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
VITE_ALCHEMY_API_KEY = 'LACONIC_HOSTED_CONFIG_aplchemy_api_key'
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'
VITE_TURNKEY_API_BASE_URL = 'LACONIC_HOSTED_CONFIG_turnkey_api_base_url'
VITE_TURNKEY_ORGANIZATION_ID = 'LACONIC_HOSTED_CONFIG_turnkey_organization_id'
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"
+4 -4
View File
@@ -4,15 +4,15 @@
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"dependencies": {},
"devDependencies": { "devDependencies": {
"depcheck": "^1.4.2",
"husky": "^8.0.3", "husky": "^8.0.3",
"lerna": "^8.0.0", "lerna": "^8.0.0",
"patch-package": "^8.0.0" "depcheck": "^1.4.2"
}, },
"scripts": { "scripts": {
"prepare": "husky install", "prepare": "husky install",
"build": "lerna run build --stream", "build": "lerna run build --stream",
"lint": "lerna run lint --stream" "lint": "lerna run lint --stream -- --max-warnings=0"
} }
} }
+1 -4
View File
@@ -25,9 +25,6 @@
"allowArgumentsExplicitlyTypedAsAny": true "allowArgumentsExplicitlyTypedAsAny": true
} }
], ],
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
"error",
{ "ignoreRestSiblings": true }
]
} }
} }
-1
View File
@@ -1,3 +1,2 @@
db db
dist dist
environments/local.toml
-1
View File
@@ -1 +0,0 @@
v20.12.1
@@ -2,11 +2,6 @@
host = "127.0.0.1" host = "127.0.0.1"
port = 8000 port = 8000
gqlPath = "/graphql" gqlPath = "/graphql"
[server.session]
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
appOriginUrl = "http://localhost:3000"
trustProxy = false
domain = "localhost"
[database] [database]
dbPath = "db/snowball" dbPath = "db/snowball"
@@ -24,11 +19,7 @@
chainId = "laconic_9000-1" chainId = "laconic_9000-1"
privateKey = "" privateKey = ""
bondId = "" bondId = ""
authority = ""
[registryConfig.fee] [registryConfig.fee]
amount = "200000" amount = "200000"
denom = "aphoton" denom = "aphoton"
gas = "750000" gas = "550000"
[misc]
projectDomain = "apps.snowballtools.com"
+13 -11
View File
@@ -1,25 +1,19 @@
{ {
"name": "backend", "name": "backend",
"license": "UNLICENSED",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@cerc-io/laconic-sdk": "^0.1.14",
"@graphql-tools/schema": "^10.0.2", "@graphql-tools/schema": "^10.0.2",
"@graphql-tools/utils": "^10.0.12", "@graphql-tools/utils": "^10.0.12",
"@octokit/oauth-app": "^6.1.0", "@octokit/oauth-app": "^6.1.0",
"@snowballtools/laconic-sdk": "^0.1.17",
"@turnkey/sdk-server": "^0.1.0",
"@types/debug": "^4.1.5", "@types/debug": "^4.1.5",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^20.11.0", "@types/node": "^20.11.0",
"@types/semver": "^7.5.8",
"apollo-server-core": "^3.13.0", "apollo-server-core": "^3.13.0",
"apollo-server-express": "^3.13.0", "apollo-server-express": "^3.13.0",
"cookie-session": "^2.1.0",
"cors": "^2.8.5",
"debug": "^4.3.1", "debug": "^4.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.18.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"luxon": "^3.4.4", "luxon": "^3.4.4",
@@ -39,21 +33,29 @@
"copy-assets": "copyfiles -u 1 src/**/*.gql dist/", "copy-assets": "copyfiles -u 1 src/**/*.gql dist/",
"clean": "rm -rf ./dist", "clean": "rm -rf ./dist",
"build": "yarn clean && tsc && yarn copy-assets", "build": "yarn clean && tsc && yarn copy-assets",
"lint": "eslint .",
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check .", "format:check": "prettier --check .",
"lint": "tsc --noEmit",
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts", "test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
"test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts", "test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts",
"test:registry:publish-deployment-removal-records": "DEBUG=snowball:* ts-node ./test/publish-deployment-removal-records.ts",
"test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts", "test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts" "test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/cookie-session": "^2.0.49",
"@types/express-session": "^1.17.10",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
"better-sqlite3": "^9.2.2", "better-sqlite3": "^9.2.2",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"prettier": "^3.1.1", "prettier": "^3.1.1",
"workspace": "^0.0.1-preview.1" "workspace": "^0.0.1-preview.1"
} }
+2 -25
View File
@@ -1,18 +1,7 @@
export interface SessionConfig {
secret: string;
appOriginUrl: string;
trustProxy: boolean;
domain: string;
}
export interface ServerConfig { export interface ServerConfig {
host: string; host: string;
port: number; port: number;
gqlPath?: string; gqlPath?: string;
sessionSecret: string;
appOriginUrl: string;
isProduction: boolean;
session: SessionConfig;
} }
export interface DatabaseConfig { export interface DatabaseConfig {
@@ -24,7 +13,7 @@ export interface GitHubConfig {
oAuth: { oAuth: {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
}; }
} }
export interface RegistryConfig { export interface RegistryConfig {
@@ -34,16 +23,11 @@ export interface RegistryConfig {
privateKey: string; privateKey: string;
bondId: string; bondId: string;
fetchDeploymentRecordDelay: number; fetchDeploymentRecordDelay: number;
authority: string;
fee: { fee: {
amount: string; amount: string;
denom: string; denom: string;
gas: string; gas: string;
}; }
}
export interface MiscConfig {
projectDomain: string;
} }
export interface Config { export interface Config {
@@ -51,11 +35,4 @@ export interface Config {
database: DatabaseConfig; database: DatabaseConfig;
gitHub: GitHubConfig; gitHub: GitHubConfig;
registryConfig: RegistryConfig; registryConfig: RegistryConfig;
misc: MiscConfig;
turnkey: {
apiBaseUrl: string;
apiPublicKey: string;
apiPrivateKey: string;
defaultOrganizationId: string;
};
} }
+6 -4
View File
@@ -1,6 +1,8 @@
import process from 'process'; export const DEFAULT_CONFIG_FILE_PATH = 'environments/local.toml';
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
export const USER_ID = '59f4355d-9549-4aac-9b54-eeefceeabef0';
export const PROJECT_DOMAIN = 'snowball.xyz';
+56 -186
View File
@@ -1,17 +1,11 @@
import { import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm';
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';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import { lowercase, numbers } from 'nanoid-dictionary'; import { lowercase, numbers } from 'nanoid-dictionary';
import { DatabaseConfig, MiscConfig } from './config'; import { DatabaseConfig } from './config';
import { User } from './entity/User'; import { User } from './entity/User';
import { Organization } from './entity/Organization'; import { Organization } from './entity/Organization';
import { Project } from './entity/Project'; import { Project } from './entity/Project';
@@ -19,10 +13,7 @@ import { Deployment } from './entity/Deployment';
import { ProjectMember } from './entity/ProjectMember'; 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 { getEntities, loadAndSaveData } from './utils'; import { PROJECT_DOMAIN } from './constants';
import { UserOrganization } from './entity/UserOrganization';
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
const log = debug('snowball:database'); const log = debug('snowball:database');
@@ -31,9 +22,8 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
// TODO: Fix order of methods // TODO: Fix order of methods
export class Database { export class Database {
private dataSource: DataSource; private dataSource: DataSource;
private projectDomain: string;
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) { constructor ({ dbPath }: DatabaseConfig) {
this.dataSource = new DataSource({ this.dataSource = new DataSource({
type: 'better-sqlite3', type: 'better-sqlite3',
database: dbPath, database: dbPath,
@@ -41,21 +31,11 @@ export class Database {
synchronize: true, synchronize: true,
logging: false logging: false
}); });
this.projectDomain = projectDomain;
} }
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({});
// Load an organization if none exist
if (!organizations.length) {
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
}
} }
async getUser (options: FindOneOptions<User>): Promise<User | null> { async getUser (options: FindOneOptions<User>): Promise<User | null> {
@@ -72,26 +52,15 @@ export class Database {
return user; return user;
} }
async updateUser (user: User, data: DeepPartial<User>): Promise<boolean> { async updateUser (userId: string, data: DeepPartial<User>): Promise<boolean> {
const userRepository = this.dataSource.getRepository(User); const userRepository = this.dataSource.getRepository(User);
const updateResult = await userRepository.update({ id: user.id }, data); const updateResult = await userRepository.update({ id: userId }, data);
assert(updateResult.affected); assert(updateResult.affected);
return updateResult.affected > 0; return updateResult.affected > 0;
} }
async getOrganizations ( async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> {
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);
@@ -114,13 +83,6 @@ export class Database {
return userOrgs; return userOrgs;
} }
async addUserOrganization (data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
const newUserOrganization = await userOrganizationRepository.save(data);
return newUserOrganization;
}
async getProjects (options: FindManyOptions<Project>): Promise<Project[]> { async getProjects (options: FindManyOptions<Project>): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository.find(options); const projects = await projectRepository.find(options);
@@ -133,11 +95,7 @@ export class Database {
const project = await projectRepository const project = await projectRepository
.createQueryBuilder('project') .createQueryBuilder('project')
.leftJoinAndSelect( .leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
'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')
@@ -150,29 +108,19 @@ export class Database {
return project; return project;
} }
async getProjectsInOrganization ( async getProjectsInOrganization (userId: string, organizationSlug: string): Promise<Project[]> {
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( .leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
'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( .where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', {
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', userId,
{ organizationSlug
userId, })
organizationSlug
}
)
.getMany(); .getMany();
return projects; return projects;
@@ -181,9 +129,7 @@ export class Database {
/** /**
* Get deployments with specified filter * Get deployments with specified filter
*/ */
async getDeployments ( async getDeployments (options: FindManyOptions<Deployment>): Promise<Deployment[]> {
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);
@@ -208,9 +154,7 @@ export class Database {
}); });
} }
async getDeployment ( async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> {
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);
@@ -238,11 +182,8 @@ export class Database {
return deployment; return deployment;
} }
async getProjectMembersByProjectId ( async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
projectId: string const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
): Promise<ProjectMember[]> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const projectMembers = await projectMemberRepository.find({ const projectMembers = await projectMemberRepository.find({
relations: { relations: {
@@ -259,12 +200,8 @@ export class Database {
return projectMembers; return projectMembers;
} }
async getEnvironmentVariablesByProjectId ( async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere<EnvironmentVariable>): Promise<EnvironmentVariable[]> {
projectId: string, const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
filter?: FindOptionsWhere<EnvironmentVariable>
): Promise<EnvironmentVariable[]> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const environmentVariables = await environmentVariableRepository.find({ const environmentVariables = await environmentVariableRepository.find({
where: { where: {
@@ -279,12 +216,9 @@ export class Database {
} }
async removeProjectMemberById (projectMemberId: string): Promise<boolean> { async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
const projectMemberRepository = const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
this.dataSource.getRepository(ProjectMember);
const deleteResult = await projectMemberRepository.delete({ const deleteResult = await projectMemberRepository.delete({ id: projectMemberId });
id: projectMemberId
});
if (deleteResult.affected) { if (deleteResult.affected) {
return deleteResult.affected > 0; return deleteResult.affected > 0;
@@ -293,63 +227,37 @@ export class Database {
} }
} }
async updateProjectMemberById ( async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> {
projectMemberId: string, const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
data: DeepPartial<ProjectMember> const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data);
): 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 ( async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> {
data: DeepPartial<ProjectMember> const projectMemberRepository = this.dataSource.getRepository(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 ( async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> {
data: DeepPartial<EnvironmentVariable>[] const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
): Promise<EnvironmentVariable[]> { const savedEnvironmentVariables = await environmentVariableRepository.save(data);
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const savedEnvironmentVariables =
await environmentVariableRepository.save(data);
return savedEnvironmentVariables; return savedEnvironmentVariables;
} }
async updateEnvironmentVariable ( async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> {
environmentVariableId: string, const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
data: DeepPartial<EnvironmentVariable> const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data);
): 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 ( async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
environmentVariableId: string const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
): Promise<boolean> { const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId });
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;
@@ -359,8 +267,7 @@ export class Database {
} }
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> { async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
const projectMemberRepository = const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
this.dataSource.getRepository(ProjectMember);
const projectMemberWithProject = await projectMemberRepository.find({ const projectMemberWithProject = await projectMemberRepository.find({
relations: { relations: {
@@ -372,7 +279,8 @@ 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');
@@ -381,49 +289,34 @@ export class Database {
return projectMemberWithProject[0]; return projectMemberWithProject[0];
} }
async getProjectsBySearchText ( async getProjectsBySearchText (userId: string, searchText: string): Promise<Project[]> {
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( .where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', {
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', userId,
{ searchText: `%${searchText}%`
userId, })
searchText: `%${searchText}%`
}
)
.getMany(); .getMany();
return projects; return projects;
} }
async updateDeploymentById ( async updateDeploymentById (deploymentId: string, data: DeepPartial<Deployment>): Promise<boolean> {
deploymentId: string,
data: DeepPartial<Deployment>
): Promise<boolean> {
return this.updateDeployment({ id: deploymentId }, data); return this.updateDeployment({ id: deploymentId }, data);
} }
async updateDeployment ( async updateDeployment (criteria: FindOptionsWhere<Deployment>, data: DeepPartial<Deployment>): Promise<boolean> {
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 ( async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> {
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
@@ -436,20 +329,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async deleteDeploymentById (deploymentId: string): Promise<boolean> { async addProject (userId: string, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.findOneOrFail({
where: {
id: deploymentId
}
});
const deleteResult = await deploymentRepository.softRemove(deployment);
return Boolean(deleteResult);
}
async addProject (user: User, organizationId: string, data: DeepPartial<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
@@ -459,26 +339,22 @@ export class Database {
// TODO: Set icon according to framework // TODO: Set icon according to framework
newProject.icon = ''; newProject.icon = '';
newProject.owner = user; newProject.owner = Object.assign(new User(), {
id: userId
});
newProject.organization = Object.assign(new Organization(), { newProject.organization = Object.assign(new Organization(), {
id: organizationId id: organizationId
}); });
newProject.subDomain = `${newProject.name}.${this.projectDomain}`; newProject.subDomain = `${newProject.name}.${PROJECT_DOMAIN}`;
return projectRepository.save(newProject); return projectRepository.save(newProject);
} }
async updateProjectById ( async updateProjectById (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
projectId: string,
data: DeepPartial<Project>
): Promise<boolean> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const updateResult = await projectRepository.update( const updateResult = await projectRepository.update({ id: projectId }, data);
{ id: projectId },
data
);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
@@ -525,20 +401,14 @@ export class Database {
return domain; return domain;
} }
async updateDomainById ( async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
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 ( async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
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({
+12 -56
View File
@@ -6,14 +6,13 @@ import {
UpdateDateColumn, UpdateDateColumn,
ManyToOne, ManyToOne,
OneToOne, OneToOne,
JoinColumn, JoinColumn
DeleteDateColumn
} from 'typeorm'; } from 'typeorm';
import { Project } from './Project'; import { Project } from './Project';
import { Domain } from './Domain'; import { Domain } from './Domain';
import { User } from './User'; import { User } from './User';
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types'; import { AppDeploymentRecordAttributes } from '../types';
export enum Environment { export enum Environment {
Production = 'Production', Production = 'Production',
@@ -25,42 +24,20 @@ export enum DeploymentStatus {
Building = 'Building', Building = 'Building',
Ready = 'Ready', Ready = 'Ready',
Error = 'Error', Error = 'Error',
Deleting = 'Deleting',
}
export interface ApplicationDeploymentRequest {
type: string;
version: string;
name: string;
application: string;
config: string;
meta: string;
}
export interface ApplicationDeploymentRemovalRequest {
type: string;
version: string;
deployment: string;
}
export interface ApplicationDeploymentRemovalRequest {
type: string;
version: string;
deployment: 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()
@@ -101,29 +78,11 @@ export class Deployment {
@Column('simple-json') @Column('simple-json')
applicationRecordData!: ApplicationRecord; applicationRecordData!: ApplicationRecord;
@Column('varchar', { nullable: true })
applicationDeploymentRequestId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
applicationDeploymentRecordId!: string | null; applicationDeploymentRecordId!: string | null;
@Column('simple-json', { nullable: true }) @Column('simple-json', { nullable: true })
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null; applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
@Column('varchar', { nullable: true })
applicationDeploymentRemovalRequestId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
@Column('varchar', { nullable: true })
applicationDeploymentRemovalRecordId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
@Column({ @Column({
enum: Environment enum: Environment
@@ -147,7 +106,4 @@ export class Deployment {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@DeleteDateColumn()
deletedAt!: Date | null;
} }
+1 -1
View File
@@ -39,7 +39,7 @@ export class Domain {
@ManyToOne(() => Domain) @ManyToOne(() => Domain)
@JoinColumn({ name: 'redirectToId' }) @JoinColumn({ name: 'redirectToId' })
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
redirectTo!: Domain | null; redirectTo!: Domain | null;
@Column({ @Column({
+3 -7
View File
@@ -27,12 +27,8 @@ export class Organization {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@OneToMany( @OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
() => UserOrganization, cascade: ['soft-remove']
(userOrganization) => userOrganization.organization, })
{
cascade: ['soft-remove']
}
)
userOrganizations!: UserOrganization[]; userOrganizations!: UserOrganization[];
} }
+16 -1
View File
@@ -15,6 +15,15 @@ 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')
@@ -43,6 +52,12 @@ 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;
@@ -76,7 +91,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[];
+1 -1
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()
+4 -19
View File
@@ -12,15 +12,10 @@ import { UserOrganization } from './UserOrganization';
@Entity() @Entity()
@Unique(['email']) @Unique(['email'])
@Unique(['ethAddress'])
export class User { export class User {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
// TODO: Set ethAddress as ID
@Column()
ethAddress!: string;
@Column('varchar', { length: 255, nullable: true }) @Column('varchar', { length: 255, nullable: true })
name!: string | null; name!: string | null;
@@ -39,23 +34,13 @@ export class User {
@CreateDateColumn() @CreateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@Column() @OneToMany(() => ProjectMember, projectMember => projectMember.project, {
subOrgId!: string;
@Column()
turnkeyWalletId!: string;
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
cascade: ['soft-remove'] cascade: ['soft-remove']
}) })
projectMembers!: ProjectMember[]; projectMembers!: ProjectMember[];
@OneToMany( @OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
() => UserOrganization, cascade: ['soft-remove']
(UserOrganization) => UserOrganization.member, })
{
cascade: ['soft-remove']
}
)
userOrganizations!: UserOrganization[]; userOrganizations!: UserOrganization[];
} }
@@ -12,7 +12,7 @@ import {
import { User } from './User'; import { User } from './User';
import { Organization } from './Organization'; import { Organization } from './Organization';
export enum Role { enum Role {
Owner = 'Owner', Owner = 'Owner',
Maintainer = 'Maintainer', Maintainer = 'Maintainer',
Reader = 'Reader', Reader = 'Reader',
+8 -12
View File
@@ -9,6 +9,8 @@ import { Database } from './database';
import { createAndStartServer } from './server'; import { createAndStartServer } from './server';
import { createResolvers } from './resolvers'; import { createResolvers } from './resolvers';
import { getConfig } from './utils'; import { getConfig } from './utils';
import { Config } from './config';
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
import { Service } from './service'; import { Service } from './service';
import { Registry } from './registry'; import { Registry } from './registry';
@@ -16,28 +18,22 @@ const log = debug('snowball:server');
const OAUTH_CLIENT_TYPE = 'oauth-app'; const OAUTH_CLIENT_TYPE = 'oauth-app';
export const main = async (): Promise<void> => { export const main = async (): Promise<void> => {
const { server, database, gitHub, registryConfig, misc } = await getConfig(); // TODO: get config path using cli
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,
clientId: gitHub.oAuth.clientId, clientId: gitHub.oAuth.clientId,
clientSecret: gitHub.oAuth.clientSecret, clientSecret: gitHub.oAuth.clientSecret
}); });
const db = new Database(database, misc); const db = new Database(database);
await db.init(); await db.init();
const registry = new Registry(registryConfig); const registry = new Registry(registryConfig);
const service = new Service( const service = new Service({ gitHubConfig: gitHub, registryConfig }, db, app, registry);
{ gitHubConfig: gitHub, registryConfig },
db,
app,
registry,
);
const typeDefs = fs const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString();
.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);
+47 -182
View File
@@ -3,80 +3,53 @@ import assert from 'assert';
import { inc as semverInc } from 'semver'; import { inc as semverInc } from 'semver';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk'; import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
import { RegistryConfig } from './config'; import { RegistryConfig } from './config';
import { import { ApplicationDeploymentRequest } from './entity/Project';
ApplicationRecord, import { ApplicationRecord, Deployment } from './entity/Deployment';
Deployment, import { AppDeploymentRecord, PackageJSON } from './types';
ApplicationDeploymentRequest,
ApplicationDeploymentRemovalRequest
} from './entity/Deployment';
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
import { sleep } from './utils';
const log = debug('snowball:registry'); const log = debug('snowball:registry');
const APP_RECORD_TYPE = 'ApplicationRecord'; const APP_RECORD_TYPE = 'ApplicationRecord';
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest'; const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'; const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
const SLEEP_DURATION = 1000;
// TODO: Move registry code to laconic-sdk/watcher-ts // TODO: Move registry code to laconic-sdk/watcher-ts
export class Registry { export class Registry {
private registry: LaconicRegistry; private registry: LaconicRegistry;
private registryConfig: RegistryConfig; private registryConfig: RegistryConfig;
constructor (registryConfig: RegistryConfig) { constructor (registryConfig : RegistryConfig) {
this.registryConfig = registryConfig; this.registryConfig = registryConfig;
this.registry = new LaconicRegistry( this.registry = new LaconicRegistry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
registryConfig.chainId
);
} }
async createApplicationRecord ({ async createApplicationRecord ({
appName,
packageJSON, packageJSON,
commitHash, commitHash,
appType, appType,
repoUrl repoUrl
}: { }: {
appName: string; packageJSON: PackageJSON
packageJSON: PackageJSON; commitHash: string,
commitHash: string; appType: string,
appType: string; repoUrl: string
repoUrl: string; }): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> {
}): Promise<{ assert(packageJSON.name, "name field doesn't exist in package.json");
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( const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId);
(record: any) => record.bondId === this.registryConfig.bondId const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
); const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch');
const [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');
@@ -87,16 +60,11 @@ export class Registry {
repository_ref: commitHash, repository_ref: commitHash,
repository: [repoUrl], repository: [repoUrl],
app_type: appType, app_type: appType,
name: appName, name: packageJSON.name,
...(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 && { ...(packageJSON.author && { author: typeof packageJSON.author === 'object' ? JSON.stringify(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 })
}; };
@@ -113,48 +81,24 @@ 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(appName); const crn = this.getCrn(packageJSON.name);
log(`Setting name: ${crn} for record ID: ${result.data.id}`); log(`Setting name: ${crn} for record ID: ${result.data.id}`);
await sleep(SLEEP_DURATION); 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 sleep(SLEEP_DURATION); return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord };
await this.registry.setName(
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
this.registryConfig.privateKey,
this.registryConfig.fee
);
await sleep(SLEEP_DURATION);
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
};
} }
async createApplicationDeploymentRequest (data: { async createApplicationDeploymentRequest (data: {
deployment: Deployment,
appName: string, appName: string,
commitHash: string,
repository: string, repository: string,
environmentVariables: { [key: string]: string }, environmentVariables: { [key: string]: string }
dns: string,
}): Promise<{ }): Promise<{
applicationDeploymentRequestId: string; applicationDeploymentRequestId: string,
applicationDeploymentRequestData: ApplicationDeploymentRequest; applicationDeploymentRequestData: ApplicationDeploymentRequest
}> { }> {
const crn = this.getCrn(data.appName); const crn = this.getCrn(data.appName);
const records = await this.registry.resolveNames([crn]); const records = await this.registry.resolveNames([crn]);
@@ -170,9 +114,9 @@ export class Registry {
version: '1.0.0', version: '1.0.0',
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`, name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
application: `${crn}@${applicationRecord.attributes.app_version}`, application: `${crn}@${applicationRecord.attributes.app_version}`,
dns: data.dns,
// TODO: Not set in test-progressive-web-app CI // TODO: Not set in test-progressive-web-app CI
// dns: '$CERC_REGISTRY_DEPLOYMENT_SHORT_HOSTNAME',
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN', // deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b // https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
@@ -180,15 +124,12 @@ export class Registry {
env: data.environmentVariables env: data.environmentVariables
}), }),
meta: JSON.stringify({ meta: JSON.stringify({
note: `Added by Snowball @ ${DateTime.utc().toFormat( note: `Added by Snowball @ ${DateTime.utc().toFormat('EEE LLL dd HH:mm:ss \'UTC\' yyyy')}`,
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
)}`,
repository: data.repository, repository: data.repository,
repository_ref: data.deployment.commitHash repository_ref: data.commitHash
}) })
}; };
await sleep(SLEEP_DURATION);
const result = await this.registry.setRecord( const result = await this.registry.setRecord(
{ {
privateKey: this.registryConfig.privateKey, privateKey: this.registryConfig.privateKey,
@@ -201,107 +142,31 @@ 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 { return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest };
applicationDeploymentRequestId: result.data.id,
applicationDeploymentRequestData: applicationDeploymentRequest
};
} }
/** /**
* Fetch ApplicationDeploymentRecords for deployments * Fetch ApplicationDeploymentRecords for deployments
*/ */
async getDeploymentRecords ( async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> {
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 ID and Deployment specific URL // Filter records with ApplicationRecord ids
return records.filter((record: AppDeploymentRecord) => return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application));
deployments.some(
(deployment) =>
deployment.applicationRecordId === record.attributes.application &&
record.attributes.url.includes(deployment.id)
)
);
} }
/** getCrn (packageJsonName: string): string {
* Fetch ApplicationDeploymentRecords by filter const [arg1, arg2] = packageJsonName.split('/');
*/
async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
return this.registry.queryRecords(
{
type: APP_DEPLOYMENT_RECORD_TYPE,
...filter
},
true
);
}
/** if (arg2) {
* Fetch ApplicationDeploymentRemovalRecords for deployments const authority = arg1.replace('@', '');
*/ return `crn://${authority}/applications/${arg2}`;
async getDeploymentRemovalRecords ( }
deployments: Deployment[]
): Promise<AppDeploymentRemovalRecord[]> {
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
const records = await this.registry.queryRecords(
{
type: APP_DEPLOYMENT_REMOVAL_RECORD_TYPE
},
true
);
// Filter records with ApplicationDeploymentRecord and ApplicationDeploymentRemovalRequest IDs return `crn://${arg1}/applications/${arg1}`;
return records.filter((record: AppDeploymentRemovalRecord) =>
deployments.some(
(deployment) =>
deployment.applicationDeploymentRemovalRequestId === record.attributes.request &&
deployment.applicationDeploymentRecordId === record.attributes.deployment
)
);
}
async createApplicationDeploymentRemovalRequest (data: {
deploymentId: string;
}): Promise<{
applicationDeploymentRemovalRequestId: string;
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
}> {
const applicationDeploymentRemovalRequest = {
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
version: '1.0.0',
deployment: data.deploymentId
};
const result = await this.registry.setRecord(
{
privateKey: this.registryConfig.privateKey,
record: applicationDeploymentRemovalRequest,
bondId: this.registryConfig.bondId
},
'',
this.registryConfig.fee
);
log(`Application deployment removal request record published: ${result.data.id}`);
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
return {
applicationDeploymentRemovalRequestId: result.data.id,
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
};
}
getCrn (appName: string): string {
assert(this.registryConfig.authority, "Authority doesn't exist");
return `crn://${this.registryConfig.authority}/applications/${appName}`;
} }
} }
+46 -164
View File
@@ -14,36 +14,26 @@ export const createResolvers = async (service: Service): Promise<any> => {
Query: { Query: {
// TODO: add custom type for context // TODO: add custom type for context
user: (_: any, __: any, context: any) => { user: (_: any, __: any, context: any) => {
return context.user; return service.getUser(context.userId);
}, },
organizations: async (_: any, __: any, context: any) => { organizations: async (_:any, __: any, context: any) => {
return service.getOrganizationsByUserId(context.user); return service.getOrganizationsByUserId(context.userId);
}, },
project: async (_: any, { projectId }: { projectId: string }) => { project: async (_: any, { projectId }: { projectId: string }) => {
return service.getProjectById(projectId); return service.getProjectById(projectId);
}, },
projectsInOrganization: async ( projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
_: any, return service.getProjectsInOrganization(context.userId, organizationSlug);
{ organizationSlug }: { organizationSlug: string },
context: any,
) => {
return service.getProjectsInOrganization(
context.user,
organizationSlug,
);
}, },
deployments: async (_: any, { projectId }: { projectId: string }) => { deployments: async (_: any, { projectId }: { projectId: string }) => {
return service.getDeploymentsByProjectId(projectId); return service.getDeploymentsByProjectId(projectId);
}, },
environmentVariables: async ( environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
_: any,
{ projectId }: { projectId: string },
) => {
return service.getEnvironmentVariablesByProjectId(projectId); return service.getEnvironmentVariablesByProjectId(projectId);
}, },
@@ -51,55 +41,32 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectMembersByProjectId(projectId); return service.getProjectMembersByProjectId(projectId);
}, },
searchProjects: async ( searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
_: any, return service.searchProjects(context.userId, searchText);
{ searchText }: { searchText: string },
context: any,
) => {
return service.searchProjects(context.user, searchText);
}, },
domains: async ( domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere<Domain> }) => {
_: 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 ( removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
_: any,
{ projectMemberId }: { projectMemberId: string },
context: any,
) => {
try { try {
return await service.removeProjectMember( return await service.removeProjectMember(context.userId, projectMemberId);
context.user,
projectMemberId,
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
updateProjectMember: async ( updateProjectMember: async (_: any, { projectMemberId, data }: {
_: any, projectMemberId: string,
{ data: {
projectMemberId, permissions: Permission[]
data, }
}: { }) => {
projectMemberId: string;
data: {
permissions: Permission[];
};
},
) => {
try { try {
return await service.updateProjectMember(projectMemberId, data); return await service.updateProjectMember(projectMemberId, data);
} catch (err) { } catch (err) {
@@ -108,19 +75,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
addProjectMember: async ( addProjectMember: async (_: any, { projectId, data }: {
_: any, projectId: string,
{ data: {
projectId, email: string,
data, permissions: Permission[]
}: { }
projectId: string; }) => {
data: {
email: string;
permissions: Permission[];
};
},
) => {
try { try {
return Boolean(await service.addProjectMember(projectId, data)); return Boolean(await service.addProjectMember(projectId, data));
} catch (err) { } catch (err) {
@@ -129,51 +90,25 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
addEnvironmentVariables: async ( addEnvironmentVariables: async (_: any, { projectId, data }: { projectId: string, data: { environments: string[], key: string, value: string}[] }) => {
_: any,
{
projectId,
data,
}: {
projectId: string;
data: { environments: string[]; key: string; value: string }[];
},
) => {
try { try {
return Boolean( return Boolean(await service.addEnvironmentVariables(projectId, data));
await service.addEnvironmentVariables(projectId, data),
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
updateEnvironmentVariable: async ( updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => {
_: any,
{
environmentVariableId,
data,
}: {
environmentVariableId: string;
data: DeepPartial<EnvironmentVariable>;
},
) => {
try { try {
return await service.updateEnvironmentVariable( return await service.updateEnvironmentVariable(environmentVariableId, data);
environmentVariableId,
data,
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
removeEnvironmentVariable: async ( removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
_: any,
{ environmentVariableId }: { environmentVariableId: string },
) => {
try { try {
return await service.removeEnvironmentVariable(environmentVariableId); return await service.removeEnvironmentVariable(environmentVariableId);
} catch (err) { } catch (err) {
@@ -182,41 +117,25 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
updateDeploymentToProd: async ( updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
_: any,
{ deploymentId }: { deploymentId: string },
context: any,
) => {
try { try {
return Boolean( return Boolean(await service.updateDeploymentToProd(context.userId, deploymentId));
await service.updateDeploymentToProd(context.user, deploymentId),
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
addProject: async ( addProject: async (_: any, { organizationSlug, data }: { organizationSlug: string, data: DeepPartial<Project> }, context: any) => {
_: any,
{
organizationSlug,
data,
}: { organizationSlug: string; data: DeepPartial<Project> },
context: any,
) => {
try { try {
return await service.addProject(context.user, organizationSlug, data); return await service.addProject(context.userId, organizationSlug, data);
} catch (err) { } catch (err) {
log(err); log(err);
throw err; throw err;
} }
}, },
updateProject: async ( updateProject: async (_: any, { projectId, data }: { projectId: string, data: DeepPartial<Project> }) => {
_: 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) {
@@ -225,15 +144,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
redeployToProd: async ( redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
_: any,
{ deploymentId }: { deploymentId: string },
context: any,
) => {
try { try {
return Boolean( return Boolean(await service.redeployToProd(context.userId, deploymentId));
await service.redeployToProd(context.user, deploymentId),
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@@ -244,8 +157,7 @@ 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); log(err); return false;
return false;
} }
}, },
@@ -258,13 +170,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
rollbackDeployment: async ( rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => {
_: 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) {
@@ -273,22 +179,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
deleteDeployment: async ( addDomain: async (_: any, { projectId, data }: { projectId: string, data: { name: string } }) => {
_: any,
{ deploymentId }: { deploymentId: string },
) => {
try {
return await service.deleteDeployment(deploymentId);
} catch (err) {
log(err);
return false;
}
},
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) {
@@ -297,10 +188,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
updateDomain: async ( updateDomain: async (_: any, { domainId, data }: { domainId: string, data: DeepPartial<Domain>}) => {
_: 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) {
@@ -309,13 +197,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
authenticateGitHub: async ( authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => {
_: any,
{ code }: { code: string },
context: any,
) => {
try { try {
return await service.authenticateGitHub(code, context.user); return await service.authenticateGitHub(code, context.userId);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@@ -324,14 +208,12 @@ 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.user, { return service.unauthenticateGitHub(context.userId, { gitHubToken: null });
gitHubToken: null,
});
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, }
}, }
}; };
}; };
-104
View File
@@ -1,104 +0,0 @@
import { Router } from 'express';
import { SiweMessage } from 'siwe';
import { Service } from '../service';
import { authenticateUser, createUser } from '../turnkey-backend';
const router = Router();
//
// Turnkey
//
router.get('/registration/:email', async (req, res) => {
const service: Service = req.app.get('service');
const user = await service.getUserByEmail(req.params.email);
if (user) {
return res.send({ subOrganizationId: user?.subOrgId });
} else {
return res.sendStatus(204);
}
});
router.post('/register', async (req, res) => {
const { email, challenge, attestation } = req.body;
const user = await createUser(req.app.get('service'), {
challenge,
attestation,
userEmail: email,
userName: email.split('@')[0],
});
req.session.userId = user.id;
res.sendStatus(200);
});
router.post('/authenticate', async (req, res) => {
const { signedWhoamiRequest } = req.body;
const user = await authenticateUser(
req.app.get('service'),
signedWhoamiRequest,
);
if (user) {
req.session.userId = user.id;
res.sendStatus(200);
} else {
res.sendStatus(401);
}
});
//
// Lit
//
router.post('/validate', async (req, res) => {
const { message, signature, action } = req.body;
const { success, data } = await new SiweMessage(message).verify({
signature,
});
if (!success) {
return res.send({ success });
}
const service: Service = req.app.get('service');
const user = await service.getUserByEthAddress(data.address);
if (action === 'signup') {
if (user) {
return res.send({ success: false, error: 'user_already_exists' });
}
const newUser = await service.createUser({
ethAddress: data.address,
email: data.address,
name: data.address,
subOrgId: data.address,
turnkeyWalletId: data.address, // TODO: fix fields
});
req.session.userId = newUser.id;
} else if (action === 'login') {
if (!user) {
return res.send({ success: false, error: 'user_not_found' });
}
req.session.userId = user.id;
}
res.send({ success });
});
//
// General
//
router.get('/session', (req, res) => {
if (req.session.userId) {
res.send({
userId: req.session.userId,
});
} else {
res.status(401).send({ error: 'Unauthorized: No active session' });
}
});
router.post('/logout', (req, res) => {
// This is how you clear cookie-session
(req as any).session = null;
res.send({ success: true });
});
export default router;
-9
View File
@@ -1,9 +0,0 @@
import { Router } from 'express';
const router = Router();
router.get('/version', async (req, res) => {
return res.send({ version: '0.0.2' });
});
export default router;
+3 -14
View File
@@ -19,7 +19,6 @@ enum DeploymentStatus {
Building Building
Ready Ready
Error Error
Deleting
} }
enum DomainStatus { enum DomainStatus {
@@ -189,19 +188,10 @@ type Query {
type Mutation { type Mutation {
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean! addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
updateProjectMember( updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
projectMemberId: String!
data: UpdateProjectMemberInput
): Boolean!
removeProjectMember(projectMemberId: String!): Boolean! removeProjectMember(projectMemberId: String!): Boolean!
addEnvironmentVariables( addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean!
projectId: String! updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean!
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!
@@ -210,7 +200,6 @@ type Mutation {
deleteProject(projectId: String!): Boolean! deleteProject(projectId: String!): Boolean!
deleteDomain(domainId: String!): Boolean! deleteDomain(domainId: String!): Boolean!
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean! rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
deleteDeployment(deploymentId: String!): Boolean!
addDomain(projectId: String!, data: AddDomainInput!): Boolean! addDomain(projectId: String!, data: AddDomainInput!): Boolean!
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean! updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
authenticateGitHub(code: String!): AuthResult! authenticateGitHub(code: String!): AuthResult!
+11 -62
View File
@@ -1,41 +1,29 @@
import debug from 'debug'; import debug from 'debug';
import express from 'express'; import express from 'express';
import cors from 'cors';
import { ApolloServer } from 'apollo-server-express'; import { ApolloServer } from 'apollo-server-express';
import { createServer } from 'http'; import { createServer } from 'http';
import { import {
ApolloServerPluginDrainHttpServer, ApolloServerPluginDrainHttpServer,
ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageLocalDefault
AuthenticationError,
} from 'apollo-server-core'; } from 'apollo-server-core';
import cookieSession from 'cookie-session';
import { TypeSource } from '@graphql-tools/utils'; import { TypeSource } from '@graphql-tools/utils';
import { makeExecutableSchema } from '@graphql-tools/schema'; import { makeExecutableSchema } from '@graphql-tools/schema';
import { ServerConfig } from './config'; import { ServerConfig } from './config';
import { DEFAULT_GQL_PATH } from './constants'; import { DEFAULT_GQL_PATH, USER_ID } from './constants';
import githubRouter from './routes/github'; import githubRouter from './routes/github';
import authRouter from './routes/auth';
import stagingRouter from './routes/staging';
import { Service } from './service'; import { Service } from './service';
const log = debug('snowball:server'); const log = debug('snowball:server');
declare module 'express-session' {
interface SessionData {
userId: string;
}
}
export const createAndStartServer = async ( export const createAndStartServer = async (
serverConfig: ServerConfig, serverConfig: ServerConfig,
typeDefs: TypeSource, typeDefs: TypeSource,
resolvers: any, resolvers: any,
service: Service, service: Service
): Promise<ApolloServer> => { ): Promise<ApolloServer> => {
const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig; const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig;
const { appOriginUrl, secret, domain, trustProxy } = serverConfig.session;
const app = express(); const app = express();
@@ -45,72 +33,33 @@ export const createAndStartServer = async (
// Create the schema // Create the schema
const schema = makeExecutableSchema({ const schema = makeExecutableSchema({
typeDefs, typeDefs,
resolvers, resolvers
}); });
const server = new ApolloServer({ const server = new ApolloServer({
schema, schema,
csrfPrevention: true, csrfPrevention: true,
context: async ({ req }) => { context: () => {
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization // TODO: Use userId derived from auth token
return { userId: USER_ID };
const { userId } = req.session;
if (!userId) {
throw new AuthenticationError('Unauthorized: No active session');
}
const user = await service.getUser(userId);
return { user };
}, },
plugins: [ plugins: [
// Proper shutdown for the HTTP server // Proper shutdown for the HTTP server
ApolloServerPluginDrainHttpServer({ httpServer }), ApolloServerPluginDrainHttpServer({ httpServer }),
ApolloServerPluginLandingPageLocalDefault({ embed: true }), ApolloServerPluginLandingPageLocalDefault({ embed: true })
], ]
}); });
await server.start(); await server.start();
app.use(
cors({
origin: appOriginUrl,
credentials: true,
}),
);
if (trustProxy) {
// trust first proxy
app.set('trust proxy', 1);
}
app.use(
cookieSession({
secret: secret,
secure: new URL(appOriginUrl).protocol === 'https:',
// 23 hours (less than 24 hours to avoid sessionSigs expiration issues)
maxAge: 23 * 60 * 60 * 1000,
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
domain: domain || undefined,
}),
);
server.applyMiddleware({ server.applyMiddleware({
app, app,
path: gqlPath, path: gqlPath
cors: {
origin: [appOriginUrl],
credentials: true,
},
}); });
app.use(express.json());
app.set('service', service); app.set('service', service);
app.use('/auth', authRouter); app.use(express.json());
app.use('/api/github', githubRouter); app.use('/api/github', githubRouter);
app.use('/staging', stagingRouter);
httpServer.listen(port, host, () => { httpServer.listen(port, host, () => {
log(`Server is listening on ${host}:${port}${server.graphqlPath}`); log(`Server is listening on ${host}:${port}${server.graphqlPath}`);
File diff suppressed because it is too large Load Diff
-130
View File
@@ -1,130 +0,0 @@
import { Turnkey, TurnkeyApiTypes } from '@turnkey/sdk-server';
// Default path for the first Ethereum address in a new HD wallet.
// See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki, paths are in the form:
// m / purpose' / coin_type' / account' / change / address_index
// - Purpose is a constant set to 44' following the BIP43 recommendation.
// - Coin type is set to 60 (ETH) -- see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
// - Account, Change, and Address Index are set to 0
import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-server';
import { getConfig } from './utils';
import { Service } from './service';
type TAttestation = TurnkeyApiTypes['v1Attestation'];
type CreateUserParams = {
userName: string;
userEmail: string;
challenge: string;
attestation: TAttestation;
};
export async function createUser(
service: Service,
{ userName, userEmail, challenge, attestation }: CreateUserParams,
) {
try {
if (await service.getUserByEmail(userEmail)) {
throw new Error(`User already exists: ${userEmail}`);
}
const config = await getConfig();
const turnkey = new Turnkey(config.turnkey);
const apiClient = turnkey.api();
const walletName = `Default ETH Wallet`;
const createSubOrgResponse = await apiClient.createSubOrganization({
subOrganizationName: `Default SubOrg for ${userEmail}`,
rootQuorumThreshold: 1,
rootUsers: [
{
userName,
userEmail,
apiKeys: [],
authenticators: [
{
authenticatorName: 'Passkey',
challenge,
attestation,
},
],
},
],
wallet: {
walletName: walletName,
accounts: DEFAULT_ETHEREUM_ACCOUNTS,
},
});
const subOrgId = refineNonNull(createSubOrgResponse.subOrganizationId);
const wallet = refineNonNull(createSubOrgResponse.wallet);
const result = {
id: wallet.walletId,
address: wallet.addresses[0],
subOrgId: subOrgId,
};
console.log('Turnkey success', result);
const user = await service.createUser({
name: userName,
email: userEmail,
subOrgId,
ethAddress: wallet.addresses[0],
turnkeyWalletId: wallet.walletId,
});
console.log('New user', user);
return user;
} catch (e) {
console.error('Failed to create user:', e);
throw e;
}
}
export async function authenticateUser(
service: Service,
signedWhoamiRequest: {
url: string;
body: any;
stamp: {
stampHeaderName: string;
stampHeaderValue: string;
};
},
) {
try {
const tkRes = await fetch(signedWhoamiRequest.url, {
method: 'POST',
body: signedWhoamiRequest.body,
headers: {
[signedWhoamiRequest.stamp.stampHeaderName]:
signedWhoamiRequest.stamp.stampHeaderValue,
},
});
console.log('AUTH RESULT', tkRes.status);
if (tkRes.status !== 200) {
console.log(await tkRes.text());
return null;
}
const orgId = (await tkRes.json()).organizationId;
const user = await service.getUserBySubOrgId(orgId);
return user;
} catch (e) {
console.error('Failed to authenticate:', e);
throw e;
}
}
function refineNonNull<T>(
input: T | null | undefined,
errorMessage?: string,
): T {
if (input == null) {
throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`);
}
return input;
}
+3 -15
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;
@@ -24,7 +24,6 @@ export interface GitPushEventPayload {
id: string; id: string;
message: string; message: string;
}; };
deleted: boolean;
} }
export interface AppDeploymentRecordAttributes { export interface AppDeploymentRecordAttributes {
@@ -38,13 +37,6 @@ export interface AppDeploymentRecordAttributes {
version: string; version: string;
} }
export interface AppDeploymentRemovalRecordAttributes {
deployment: string;
request: string;
type: "ApplicationDeploymentRemovalRecord";
version: string;
}
interface RegistryRecord { interface RegistryRecord {
id: string; id: string;
names: string[] | null; names: string[] | null;
@@ -55,9 +47,5 @@ interface RegistryRecord {
} }
export interface AppDeploymentRecord extends RegistryRecord { export interface AppDeploymentRecord extends RegistryRecord {
attributes: AppDeploymentRecordAttributes; attributes: AppDeploymentRecordAttributes
}
export interface AppDeploymentRemovalRecord extends RegistryRecord {
attributes: AppDeploymentRemovalRecordAttributes;
} }
+2 -59
View File
@@ -2,19 +2,11 @@ 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';
import { Config } from './config';
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
const log = debug('snowball:utils'); const log = debug('snowball:utils');
export async function getConfig() { export const getConfig = async <ConfigType>(
// TODO: get config path using cli configFile: string
return await _getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
}
const _getConfig = async <ConfigType>(
configFile: string,
): Promise<ConfigType> => { ): Promise<ConfigType> => {
const configFilePath = path.resolve(configFile); const configFilePath = path.resolve(configFile);
const fileExists = await fs.pathExists(configFilePath); const fileExists = await fs.pathExists(configFilePath);
@@ -27,52 +19,3 @@ 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;
};
export const sleep = async (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
+1 -1
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));
+20 -42
View File
@@ -1,16 +1,14 @@
[ [
{ {
"projectIndex": 0, "projectIndex": 0,
"domainIndex": 0, "domainIndex":0,
"createdByIndex": 0, "createdByIndex": 0,
"id": "ffhae3zq", "id":"ffhae3zq",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"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",
@@ -18,16 +16,14 @@
}, },
{ {
"projectIndex": 0, "projectIndex": 0,
"domainIndex": 1, "domainIndex":1,
"createdByIndex": 0, "createdByIndex": 0,
"id": "vehagei8", "id":"vehagei8",
"status": "Ready", "status": "Ready",
"environment": "Preview", "environment": "Preview",
"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",
@@ -35,16 +31,14 @@
}, },
{ {
"projectIndex": 0, "projectIndex": 0,
"domainIndex": 2, "domainIndex":2,
"createdByIndex": 0, "createdByIndex": 0,
"id": "qmgekyte", "id":"qmgekyte",
"status": "Ready", "status": "Ready",
"environment": "Development", "environment": "Development",
"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,14 +48,12 @@
"projectIndex": 0, "projectIndex": 0,
"domainIndex": null, "domainIndex": null,
"createdByIndex": 0, "createdByIndex": 0,
"id": "f8wsyim6", "id":"f8wsyim6",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"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,16 +61,14 @@
}, },
{ {
"projectIndex": 1, "projectIndex": 1,
"domainIndex": 3, "domainIndex":3,
"createdByIndex": 1, "createdByIndex": 1,
"id": "eO8cckxk", "id":"eO8cckxk",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"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",
@@ -86,16 +76,14 @@
}, },
{ {
"projectIndex": 1, "projectIndex": 1,
"domainIndex": 4, "domainIndex":4,
"createdByIndex": 1, "createdByIndex": 1,
"id": "yaq0t5yw", "id":"yaq0t5yw",
"status": "Ready", "status": "Ready",
"environment": "Preview", "environment": "Preview",
"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",
@@ -103,16 +91,14 @@
}, },
{ {
"projectIndex": 1, "projectIndex": 1,
"domainIndex": 5, "domainIndex":5,
"createdByIndex": 1, "createdByIndex": 1,
"id": "hwwr6sbx", "id":"hwwr6sbx",
"status": "Ready", "status": "Ready",
"environment": "Development", "environment": "Development",
"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",
@@ -120,16 +106,14 @@
}, },
{ {
"projectIndex": 2, "projectIndex": 2,
"domainIndex": 9, "domainIndex":9,
"createdByIndex": 2, "createdByIndex": 2,
"id": "ndxje48a", "id":"ndxje48a",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"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",
@@ -137,16 +121,14 @@
}, },
{ {
"projectIndex": 2, "projectIndex": 2,
"domainIndex": 7, "domainIndex":7,
"createdByIndex": 2, "createdByIndex": 2,
"id": "gtgpgvei", "id":"gtgpgvei",
"status": "Ready", "status": "Ready",
"environment": "Preview", "environment": "Preview",
"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",
@@ -154,16 +136,14 @@
}, },
{ {
"projectIndex": 2, "projectIndex": 2,
"domainIndex": 8, "domainIndex":8,
"createdByIndex": 2, "createdByIndex": 2,
"id": "b4bpthjr", "id":"b4bpthjr",
"status": "Ready", "status": "Ready",
"environment": "Development", "environment": "Development",
"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",
@@ -173,14 +153,12 @@
"projectIndex": 3, "projectIndex": 3,
"domainIndex": 6, "domainIndex": 6,
"createdByIndex": 2, "createdByIndex": 2,
"id": "b4bpthjr", "id":"b4bpthjr",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"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",
+31 -9
View File
@@ -2,55 +2,77 @@
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 0, "projectIndex": 0,
"permissions": ["View"], "permissions": [
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 0, "projectIndex": 0,
"permissions": ["View", "Edit"], "permissions": [
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 1, "projectIndex": 1,
"permissions": ["View"], "permissions": [
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 0, "memberIndex": 0,
"projectIndex": 2, "projectIndex": 2,
"permissions": ["View"], "permissions": [
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 2, "projectIndex": 2,
"permissions": ["View", "Edit"], "permissions": [
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 0, "memberIndex": 0,
"projectIndex": 3, "projectIndex": 3,
"permissions": ["View"], "permissions": [
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 3, "projectIndex": 3,
"permissions": ["View", "Edit"], "permissions": [
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 4, "projectIndex": 4,
"permissions": ["View"], "permissions": [
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 4, "projectIndex": 4,
"permissions": ["View", "Edit"], "permissions": [
"View",
"Edit"
],
"isPending": false "isPending": false
} }
] ]
+10
View File
@@ -10,6 +10,8 @@
"framework": "test", "framework": "test",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "testProject.snowball.xyz" "subDomain": "testProject.snowball.xyz"
}, },
{ {
@@ -23,6 +25,8 @@
"framework": "test-2", "framework": "test-2",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "testProject-2.snowball.xyz" "subDomain": "testProject-2.snowball.xyz"
}, },
{ {
@@ -36,6 +40,8 @@
"framework": "test-3", "framework": "test-3",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "iglootools.snowball.xyz" "subDomain": "iglootools.snowball.xyz"
}, },
{ {
@@ -49,6 +55,8 @@
"framework": "test-4", "framework": "test-4",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "iglootools-2.snowball.xyz" "subDomain": "iglootools-2.snowball.xyz"
}, },
{ {
@@ -62,6 +70,8 @@
"framework": "test-5", "framework": "test-5",
"webhooks": [], "webhooks": [],
"icon": "", "icon": "",
"applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "snowball-2.snowball.xyz" "subDomain": "snowball-2.snowball.xyz"
} }
] ]
+6 -9
View File
@@ -1,23 +1,20 @@
[ [
{ {
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0", "id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
"name": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "name": "Snowball",
"email": "snowball@snowballtools.xyz", "email": "snowball@snowballtools.xyz",
"isVerified": true, "isVerified": true
"ethAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
}, },
{ {
"id": "e505b212-8da6-48b2-9614-098225dab34b", "id": "e505b212-8da6-48b2-9614-098225dab34b",
"name": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8", "name": "Alice Anderson",
"email": "alice@snowballtools.xyz", "email": "alice@snowballtools.xyz",
"isVerified": true, "isVerified": true
"ethAddress": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"
}, },
{ {
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7", "id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
"name": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a", "name": "Bob Banner",
"email": "bob@snowballtools.xyz", "email": "bob@snowballtools.xyz",
"isVerified": true, "isVerified": true
"ethAddress": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a"
} }
] ]
+49 -83
View File
@@ -1,4 +1,5 @@
import { DataSource } from 'typeorm'; import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
import * as fs from 'fs/promises';
import debug from 'debug'; import debug from 'debug';
import path from 'path'; import path from 'path';
@@ -10,12 +11,7 @@ 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 { import { getConfig } from '../src/utils';
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';
@@ -31,35 +27,43 @@ 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 generateTestData = async (dataSource: DataSource) => { const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, filePath: string, relations?: any | undefined) => {
const userEntities = await getEntities( const entitiesData = await fs.readFile(filePath, 'utf-8');
path.resolve(__dirname, USER_DATA_PATH) const entities = JSON.parse(entitiesData);
); const entityRepository = dataSource.getRepository(entityType);
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
const orgEntities = await getEntities( const savedEntity:Entity[] = [];
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
); for (const entityData of entities) {
const savedOrgs = await loadAndSaveData( let entity = entityRepository.create(entityData as DeepPartial<Entity>);
Organization,
dataSource, if (relations) {
orgEntities for (const field in relations) {
); const valueIndex = String(field + 'Index');
entity = {
...entity,
[field]: relations[field][entityData[valueIndex]]
};
}
}
const dbEntity = await entityRepository.save(entity);
savedEntity.push(dbEntity);
}
return savedEntity;
};
const generateTestData = async (dataSource: DataSource) => {
const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH));
const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH));
const projectRelations = { const projectRelations = {
owner: savedUsers, owner: savedUsers,
organization: savedOrgs organization: savedOrgs
}; };
const projectEntities = await getEntities( const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations);
path.resolve(__dirname, PROJECT_DATA_PATH)
);
const savedProjects = await loadAndSaveData(
Project,
dataSource,
projectEntities,
projectRelations
);
const domainRepository = dataSource.getRepository(Domain); const domainRepository = dataSource.getRepository(Domain);
@@ -67,30 +71,14 @@ const generateTestData = async (dataSource: DataSource) => {
project: savedProjects project: savedProjects
}; };
const primaryDomainsEntities = await getEntities( const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations);
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
}; };
const redirectDomainsEntities = await getEntities( await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations);
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
);
await loadAndSaveData(
Domain,
dataSource,
redirectDomainsEntities,
domainRedirectedRelations
);
const savedDomains = await domainRepository.find(); const savedDomains = await domainRepository.find();
@@ -99,30 +87,14 @@ const generateTestData = async (dataSource: DataSource) => {
organization: savedOrgs organization: savedOrgs
}; };
const userOrganizationsEntities = await getEntities( await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations);
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
}; };
const projectMembersEntities = await getEntities( await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations);
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
);
await loadAndSaveData(
ProjectMember,
dataSource,
projectMembersEntities,
projectMemberRelations
);
const deploymentRelations = { const deploymentRelations = {
project: savedProjects, project: savedProjects,
@@ -130,29 +102,23 @@ const generateTestData = async (dataSource: DataSource) => {
createdBy: savedUsers createdBy: savedUsers
}; };
const deploymentsEntities = await getEntities( await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations);
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
);
await loadAndSaveData(
Deployment,
dataSource,
deploymentsEntities,
deploymentRelations
);
const environmentVariableRelations = { const environmentVariableRelations = {
project: savedProjects project: savedProjects
}; };
const environmentVariablesEntities = await getEntities( await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations);
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH) };
);
await loadAndSaveData( const checkFileExists = async (filePath: string) => {
EnvironmentVariable, try {
dataSource, await fs.access(filePath, fs.constants.F_OK);
environmentVariablesEntities, return true;
environmentVariableRelations } catch (err) {
); log(err);
return false;
}
}; };
const main = async () => { const main = async () => {
+7 -15
View File
@@ -1,6 +1,6 @@
import debug from 'debug'; import debug from 'debug';
import { Registry } from '@snowballtools/laconic-sdk'; import { Registry } from '@cerc-io/laconic-sdk';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants'; import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
import { Config } from '../src/config'; import { Config } from '../src/config';
@@ -11,30 +11,22 @@ const log = debug('snowball:initialize-registry');
const DENOM = 'aphoton'; const DENOM = 'aphoton';
const BOND_AMOUNT = '1000000000'; const BOND_AMOUNT = '1000000000';
// TODO: Get authority names from args
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);
// TODO: Get authority names from args
const authorityNames = ['snowballtools', registryConfig.authority];
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( await registry.createBond({ denom: DENOM, amount: BOND_AMOUNT }, registryConfig.privateKey, registryConfig.fee);
{ denom: DENOM, amount: BOND_AMOUNT },
registryConfig.privateKey,
registryConfig.fee
);
for await (const name of authorityNames) { 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( await registry.setAuthorityBond({ name, bondId }, registryConfig.privateKey, registryConfig.fee);
{ name, bondId },
registryConfig.privateKey,
registryConfig.fee
);
log(`Bond ${bondId} set for authority ${name}`); log(`Bond ${bondId} set for authority ${name}`);
} }
} }
@@ -2,23 +2,19 @@ import debug from 'debug';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import path from 'path'; import path from 'path';
import { Registry } from '@snowballtools/laconic-sdk'; import { Registry } from '@cerc-io/laconic-sdk';
import { Config } from '../src/config'; import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants'; import { DEFAULT_CONFIG_FILE_PATH, PROJECT_DOMAIN } from '../src/constants';
import { getConfig } from '../src/utils'; import { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment'; 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, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); const { registryConfig, database } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry( const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
registryConfig.chainId
);
const dataSource = new DataSource({ const dataSource = new DataSource({
type: 'better-sqlite3', type: 'better-sqlite3',
@@ -40,7 +36,7 @@ async function main () {
}); });
for await (const deployment of deployments) { for await (const deployment of deployments) {
const url = `https://${deployment.project.name}-${deployment.id}.${misc.projectDomain}`; const url = `${deployment.project.name}-${deployment.id}.${PROJECT_DOMAIN}`;
const applicationDeploymentRecord = { const applicationDeploymentRecord = {
type: 'ApplicationDeploymentRecord', type: 'ApplicationDeploymentRecord',
@@ -57,7 +53,7 @@ async function main () {
so: '66fcfa49a1664d4cb4ce4f72c1c0e151' so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
}), }),
request: deployment.applicationDeploymentRequestId, request: deployment.project.applicationDeploymentRequestId,
url url
}; };
@@ -71,21 +67,6 @@ async function main () {
registryConfig.fee registryConfig.fee
); );
// Remove deployment for project subdomain if deployment is for production environment
if (deployment.environment === Environment.Production) {
applicationDeploymentRecord.url = `https://${deployment.project.subDomain}`
await registry.setRecord(
{
privateKey: registryConfig.privateKey,
record: applicationDeploymentRecord,
bondId: registryConfig.bondId
},
'',
registryConfig.fee
);
}
log('Application deployment record data:', applicationDeploymentRecord); log('Application deployment record data:', applicationDeploymentRecord);
log(`Application deployment record published: ${result.data.id}`); log(`Application deployment record published: ${result.data.id}`);
} }
@@ -1,67 +0,0 @@
import debug from 'debug';
import { DataSource } from 'typeorm';
import path from 'path';
import { Registry } from '@cerc-io/laconic-sdk';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
import { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
const log = debug('snowball:publish-deployment-removal-records');
async function main () {
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
registryConfig.chainId
);
const dataSource = new DataSource({
type: 'better-sqlite3',
database: database.dbPath,
synchronize: true,
entities: [path.join(__dirname, '../src/entity/*')]
});
await dataSource.initialize();
const deploymentRepository = dataSource.getRepository(Deployment);
const deployments = await deploymentRepository.find({
relations: {
project: true
},
where: {
status: DeploymentStatus.Deleting
}
});
for await (const deployment of deployments) {
const applicationDeploymentRemovalRecord = {
type: "ApplicationDeploymentRemovalRecord",
version: "1.0.0",
deployment: deployment.applicationDeploymentRecordId,
request: deployment.applicationDeploymentRemovalRequestId,
}
const result = await registry.setRecord(
{
privateKey: registryConfig.privateKey,
record: applicationDeploymentRemovalRecord,
bondId: registryConfig.bondId
},
'',
registryConfig.fee
);
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
log(`Application deployment removal record published: ${result.data.id}`);
}
}
main().catch((err) => {
log(err);
});
-57
View File
@@ -1,57 +0,0 @@
# deployer
- Install dependencies
```bash
yarn
```
```bash
brew install jq # if you do not have jq installed already
```
- Run script to deploy app
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
```
./deploy-frontend.staging.sh
```
- To deploy frontend app to `dashboard.apps.snowballtools.com`
```
./deploy-frontend.sh
```
- Commit the updated [ApplicationRecord](records/application-record.yml) and [ApplicationDeploymentRequest](records/application-deployment-request.yml) files to the repository
## Notes
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
```yml
record:
...
config:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
...
```
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
## Troubleshoot
- Check deployment status in [web-app deployer](https://console.laconic.com/deployer).
- Check records in [registry console app](https://console.laconic.com/#/registry).
- If deployment fails due to low bond balance
- Check balances
```bash
# Account balance
yarn laconic cns account get
# Bond balance
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
```
- Command to refill bond
```bash
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
```
-9
View File
@@ -1,9 +0,0 @@
services:
cns:
restEndpoint: 'http://console.laconic.com:1317'
gqlEndpoint: 'http://console.laconic.com:9473/api'
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
bondId: 098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661
chainId: laconic_9000-1
gas: 550000
fees: 200000aphoton
-9
View File
@@ -1,9 +0,0 @@
services:
cns:
restEndpoint: http://console.laconic.com:1317
gqlEndpoint: http://console.laconic.com:9473/api
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
chainId: laconic_9000-1
gas: 1200000
fees: 200000aphoton
-95
View File
@@ -1,95 +0,0 @@
#!/bin/bash
# Repository URL
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
# Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
# Extract version from ../frontend/package.json
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
# Current date and time for note
CURRENT_DATE_TIME=$(date -u)
CONFIG_FILE=config.yml
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
# Get latest version from registry and increment application-record version
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found
NEW_APPLICATION_VERSION=0.0.1
fi
# Generate application-deployment-request.yml
cat > ./records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: snowballtools-base-frontend@$PACKAGE_VERSION
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
repository_ref: $LATEST_HASH
EOF
# Generate application-record.yml with incremented version
cat > ./records/application-record.yml <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
repository_ref: $LATEST_HASH
repository: ["$REPO_URL"]
app_type: webapp
name: snowballtools-base-frontend
app_version: $PACKAGE_VERSION
EOF
echo "Files generated successfully."
RECORD_FILE=records/application-record.yml
# Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
# Set name to record
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
sleep 2
# Set name if latest release
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN."
exit 1
fi
RECORD_FILE=records/application-deployment-request.yml
sleep 2
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
@@ -1,101 +0,0 @@
#!/bin/bash
# Repository URL
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
# Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
# Extract version from ../frontend/package.json
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
# Current date and time for note
CURRENT_DATE_TIME=$(date -u)
CONFIG_FILE=config.staging.yml
REGISTRY_BOND_ID="098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661"
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
# Get latest version from registry and increment application-record version
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "staging-snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found
NEW_APPLICATION_VERSION=0.0.1
fi
# Generate application-deployment-request.yml
cat > ./staging-records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
dns: dashboard.staging.apps.snowballtools.com
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
repository_ref: $LATEST_HASH
EOF
# Generate application-record.yml with incremented version
cat > ./staging-records/application-record.yml <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
repository_ref: $LATEST_HASH
repository: ["$REPO_URL"]
app_type: webapp
name: staging-snowballtools-base-frontend
app_version: $PACKAGE_VERSION
EOF
echo "Files generated successfully."
RECORD_FILE=staging-records/application-record.yml
# Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
# Set name to record
REGISTRY_APP_CRN="crn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
sleep 2
# Set name if latest release
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN."
exit 1
fi
RECORD_FILE=staging-records/application-deployment-request.yml
sleep 2
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
-9
View File
@@ -1,9 +0,0 @@
{
"name": "deployer",
"version": "1.0.0",
"main": "index.js",
"private": true,
"devDependencies": {
"@snowballtools/laconic-registry-cli": "^0.1.13"
}
}
@@ -1,18 +0,0 @@
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: snowballtools-base-frontend@0.1.8
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
@@ -1,8 +0,0 @@
record:
type: ApplicationRecord
version: 0.0.2
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: snowballtools-base-frontend
app_version: 0.1.8
@@ -1,24 +0,0 @@
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: staging-snowballtools-base-frontend@0.0.0
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@0.0.0
dns: dashboard.staging.apps.snowballtools.com
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta:
note: Added by Snowball @ Tuesday 21 May 2024 06:17:23 AM UTC
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
@@ -1,8 +0,0 @@
record:
type: ApplicationRecord
version: 0.0.2
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: staging-snowballtools-base-frontend
app_version: 0.0.0
-23
View File
@@ -1,23 +0,0 @@
# deployer test
Check if the live web app deployer is in a working state
- Web app repo used: https://github.com/snowball-tools-platform/test-progressive-web-app (main branch)
- Config used: [../config.yml](../config.yml)
- The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following:
- Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo
- Fetch the latest version of `deployment-test-app` from registry and increment `ApplicationRecord` version
- Publish the resulting `ApplicationRecord` record
- Set names to the record and check name resolution
- Publish the `ApplicationDeploymentRequest` record
- Check that the deployment occurs
- Check that a `ApplicationDeploymentRecord` is created
- Check that the deployment record has correct `ApplicationRecord` id
- Check that the URL present in deployment record is active
- Create and publish a [`ApplicationDeploymentRemovalRequest`](./records/application-deployment-removal-request.yml) record
- Check that the deployment is removed
- Check that a `ApplicationDeploymentRemovalRecord` is created
- Check that the deployment URL goes down
- The test script is run in a GitHub CI [workflow](../../../.github/workflows/test-app-deployment.yaml) that:
- Is scheduled to run everyday on the default (`main`) branch or can be triggered manually
- Sends Slack alerts to configured channels on failure
@@ -1,4 +0,0 @@
record:
deployment: <APPLICATION_DEPLOYMENT_RECORD_ID>
type: ApplicationDeploymentRemovalRequest
version: 1.0.0
@@ -1,15 +0,0 @@
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: deployment-test-app@0.1.24
application: crn://snowballtools/applications/deployment-test-app@0.1.24
dns: deployment-ci-test
config:
env:
CERC_TEST_WEBAPP_CONFIG1: "deployment test config 1"
CERC_TEST_WEBAPP_CONFIG2: "deployment test config 2"
CERC_WEBAPP_DEBUG: 0
meta:
note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC
repository: "https://github.com/snowball-tools-platform/test-progressive-web-app"
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
@@ -1,8 +0,0 @@
record:
type: ApplicationRecord
version: 0.0.1
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
repository: ["https://github.com/snowball-tools-platform/test-progressive-web-app"]
app_type: webapp
name: deployment-test-app
app_version: 0.1.24
@@ -1,225 +0,0 @@
#!/bin/bash
# Repository URL
REPO_URL="https://github.com/snowball-tools-platform/test-progressive-web-app"
# Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
# Fetch the package.json file content
# Extract version from package.json content
package_json=$(wget -qO- "$REPO_URL/raw/$LATEST_HASH/package.json")
PACKAGE_VERSION=$(echo "$package_json" | jq -r '.version')
# Current date and time for note
CURRENT_DATE_TIME=$(date -u)
CONFIG_FILE=packages/deployer/config.yml
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
APP_NAME=deployment-test-app
# Get latest version from registry and increment application-record version
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found
NEW_APPLICATION_VERSION=0.0.1
fi
# Generate application-record.yml with incremented version
RECORD_FILE=packages/deployer/test/records/application-record.yml
cat > $RECORD_FILE <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
repository_ref: $LATEST_HASH
repository: ["$REPO_URL"]
app_type: webapp
name: $APP_NAME
app_version: $PACKAGE_VERSION
EOF
# Generate application-deployment-request.yml
REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-request.yml
cat > $REQUEST_RECORD_FILE <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: $APP_NAME@$PACKAGE_VERSION
application: crn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
dns: deployment-ci-test
config:
env:
CERC_TEST_WEBAPP_CONFIG1: "deployment test config 1"
CERC_TEST_WEBAPP_CONFIG2: "deployment test config 2"
CERC_WEBAPP_DEBUG: 0
meta:
note: Deployment test @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
repository_ref: $LATEST_HASH
EOF
echo "Record files generated successfully."
# Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
# Set name to record
REGISTRY_APP_CRN="crn://snowballtools/applications/$APP_NAME"
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
sleep 2
# Set name if latest release
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record exists for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN."
exit 1
fi
sleep 2
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
# Deployment checks
RETRY_INTERVAL=30
MAX_RETRIES=20
# Check that a ApplicationDeploymentRecord is published
retry_count=0
while true; do
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
len_deployment_records=$(echo $deployment_records_response | jq 'length')
# Check if number of records returned is 0
if [ $len_deployment_records -eq 0 ]; then
# Check if retries are exhausted
if [ $retry_count -eq $MAX_RETRIES ]; then
echo "Retries exhausted"
echo "ApplicationDeploymentRecord for deployment request $DEPLOYMENT_REQUEST_ID not found, exiting"
exit 1
else
echo "ApplicationDeploymentRecord not found, retrying in $RETRY_INTERVAL sec..."
sleep $RETRY_INTERVAL
retry_count=$((retry_count+1))
fi
else
echo "ApplicationDeploymentRecord found"
break
fi
done
DEPLOYMENT_RECORD_ID=$(echo $deployment_records_response | jq -r '.[0].id')
echo $DEPLOYMENT_RECORD_ID
# Check if ApplicationDeploymentRecord has the correct record id
fetched_application_record_id=$(echo $deployment_records_response | jq -r '.[0].attributes.application')
if [ "$fetched_application_record_id" = "$RECORD_ID" ]; then
echo "ApplicationRecord id matched"
else
echo "ApplicationRecord id does not match, expected: $RECORD_ID, received: $fetched_application_record_id"
exit 1
fi
# Check if the url present in ApplicationDeploymentRecord is active
fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
retry_count=0
max_retries=10
retry_interval=5
while true; do
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
if [ "$url_response" = "200" ]; then
echo "Deployment URL $fetched_url is active"
break
else
if [ $retry_count -eq $max_retries ]; then
echo "Retries exhausted"
echo "Deployment URL $fetched_url is not active, exiting"
exit 1
else
echo "Deployment URL $fetched_url is not active, received code $url_response, retrying in $retry_interval sec..."
sleep $retry_interval
retry_count=$((retry_count+1))
fi
fi
done
# Generate application-deployment-removal-request.yml
REMOVAL_REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-removal-request.yml
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
record:
deployment: $DEPLOYMENT_RECORD_ID
type: ApplicationDeploymentRemovalRequest
version: 1.0.0
EOF
sleep 2
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRemovalRequest published"
echo $REMOVAL_REQUEST_ID
# Check that an ApplicationDeploymentRemovalRecord is published
retry_count=0
while true; do
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
len_removal_records=$(echo $removal_records_response | jq 'length')
# Check if number of records returned is 0
if [ $len_removal_records -eq 0 ]; then
# Check if retries are exhausted
if [ $retry_count -eq $MAX_RETRIES ]; then
echo "Retries exhausted"
echo "ApplicationDeploymentRemovalRecord for deployment removal request $REMOVAL_REQUEST_ID not found"
exit 1
else
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
sleep $RETRY_INTERVAL
retry_count=$((retry_count+1))
fi
else
echo "ApplicationDeploymentRemovalRecord found"
REMOVAL_RECORD_ID=$(echo $removal_records_response | jq -r '.[0].id')
echo $REMOVAL_RECORD_ID
break
fi
done
# Check if the application url is down after deployment removal
retry_count=0
max_retries=10
retry_interval=5
while true; do
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
if [ "$url_response" = "404" ]; then
echo "Deployment URL $fetched_url is down"
break
else
if [ $retry_count -eq $max_retries ]; then
echo "Retries exhausted"
echo "Deployment URL $fetched_url is still active, exiting"
exit 1
else
echo "Deployment URL $fetched_url is still active, received code $url_response, retrying in $retry_interval sec..."
sleep $retry_interval
retry_count=$((retry_count+1))
fi
fi
done
echo "Test successful"
+4
View File
@@ -0,0 +1,4 @@
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
REACT_APP_GITHUB_CLIENT_ID =
REACT_APP_GITHUB_TEMPLATE_REPO =
-17
View File
@@ -1,17 +0,0 @@
VITE_SERVER_URL='http://localhost:8000'
VITE_GITHUB_CLIENT_ID=
VITE_GITHUB_PWA_TEMPLATE_REPO="snowball-tools/test-progressive-web-app"
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO="snowball-tools/image-upload-pwa-example"
VITE_WALLET_CONNECT_ID=
VITE_LIT_RELAY_API_KEY=
VITE_ALCHEMY_API_KEY=
VITE_BUGSNAG_API_KEY=
VITE_PASSKEY_WALLET_RPID=
VITE_TURNKEY_API_BASE_URL=
VITE_TURNKEY_ORGANIZATION_ID=
+1
View File
@@ -0,0 +1 @@
build
-19
View File
@@ -1,19 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:storybook/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
};
+20
View File
@@ -0,0 +1,20 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 13,
"sourceType": "module"
},
"env": {
"browser": true,
"es2021": true
},
"plugins": ["react", "@typescript-eslint"],
"extends": [
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
]
}
-2
View File
@@ -13,7 +13,6 @@
# misc # misc
.DS_Store .DS_Store
.env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
@@ -22,4 +21,3 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
*storybook.log
-1
View File
@@ -1 +0,0 @@
v20.12.1
+3 -1
View File
@@ -1 +1,3 @@
dist/ # artifacts
build
coverage
-31
View File
@@ -1,31 +0,0 @@
import type { StorybookConfig } from '@storybook/react-vite';
import { join, dirname } from 'path';
/**
* This function is used to resolve the absolute path of a package.
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
*/
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')));
}
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
getAbsolutePath('@storybook/addon-onboarding'),
getAbsolutePath('@storybook/addon-links'),
getAbsolutePath('@storybook/addon-essentials'),
getAbsolutePath('@chromatic-com/storybook'),
getAbsolutePath('@storybook/addon-interactions'),
getAbsolutePath('storybook-addon-remix-react-router'),
],
framework: {
name: getAbsolutePath('@storybook/react-vite'),
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;
-16
View File
@@ -1,16 +0,0 @@
import type { Preview } from '@storybook/react';
import '../src/index.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
+39
View File
@@ -0,0 +1,39 @@
{
// eslint extension options
"eslint.enable": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"css.customData": [".vscode/tailwind.json"],
// prettier extension setting
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.rulers": [80],
"editor.codeActionsOnSave": [
"source.addMissingImports",
"source.fixAll",
"source.organizeImports"
],
// Show in vscode "Problems" tab when there are errors
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
// Use absolute import for typescript files
"typescript.preferences.importModuleSpecifier": "non-relative",
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
+38 -22
View File
@@ -1,30 +1,46 @@
# React + TypeScript + Vite # Getting Started with Create React App
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using [typescript-tailwindcss-eslint-prettier](https://github.com/cufarvid/cra-templates) template.
Currently, two official plugins are available: ## Available Scripts
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh In the project directory, you can run:
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration ### `yarn start`
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
- Configure the top-level `parserOptions` property like this: The page will reload if you make edits.\
You will also see any lint errors in the console.
```js ### `yarn test`
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
};
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` Launches the test runner in the interactive watch mode.\
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
-4
View File
@@ -1,4 +0,0 @@
{
"projectId": "Project:663d04870db27ed66a48e466",
"zip": true
}
-22
View File
@@ -1,22 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="snowball tools dashboard" />
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="msapplication-TileColor" content="#2d89ef" />
<meta name="theme-color" content="#ffffff" />
<link rel="manifest" href="/manifest.json" />
<title>Snowball</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+47 -70
View File
@@ -1,58 +1,25 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.1.0",
"private": true, "private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"build": "vite build",
"lint": "tsc --noEmit",
"preview": "vite preview",
"format": "prettier --write .",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": { "dependencies": {
"@bugsnag/browser-performance": "^2.4.1",
"@bugsnag/js": "^7.22.7",
"@bugsnag/plugin-react": "^7.22.7",
"@fontsource-variable/jetbrains-mono": "^5.0.19",
"@fontsource/inter": "^5.0.16", "@fontsource/inter": "^5.0.16",
"@material-tailwind/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@snowballtools/auth": "^0.2.0",
"@snowballtools/auth-lit": "^0.2.0",
"@snowballtools/js-sdk": "^0.1.1",
"@snowballtools/link-lit-alchemy-light": "^0.2.0",
"@snowballtools/material-tailwind-react-fork": "^2.1.10",
"@snowballtools/smartwallet-alchemy-light": "^0.2.0",
"@snowballtools/types": "^0.2.0",
"@snowballtools/utils": "^0.1.1",
"@tanstack/react-query": "^5.22.2",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@turnkey/http": "^2.10.0", "@types/jest": "^27.5.2",
"@turnkey/sdk-react": "^0.1.0", "@types/node": "^16.18.68",
"@turnkey/webauthn-stamper": "^0.5.0", "@types/react": "^18.2.42",
"@walletconnect/ethereum-provider": "^2.12.2", "@types/react-dom": "^18.2.17",
"@web3modal/siwe": "^4.0.5",
"@web3modal/wagmi": "^4.0.5",
"assert": "^2.1.0", "assert": "^2.1.0",
"axios": "^1.6.7",
"clsx": "^2.1.0",
"date-fns": "^3.3.1", "date-fns": "^3.3.1",
"downshift": "^8.3.2", "downshift": "^8.2.3",
"framer-motion": "^11.0.8", "eslint-config-react-app": "^7.0.1",
"gql-client": "^1.0.0", "gql-client": "^1.0.0",
"lottie-react": "^2.4.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"octokit": "^3.1.2", "octokit": "^3.1.2",
"react": "^18.2.0", "react": "^18.2.0",
@@ -65,41 +32,51 @@
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-oauth-popup": "^1.0.5", "react-oauth-popup": "^1.0.5",
"react-router-dom": "^6.20.1", "react-router-dom": "^6.20.1",
"react-scripts": "5.0.1",
"react-timer-hook": "^3.0.7", "react-timer-hook": "^3.0.7",
"siwe": "^2.1.4",
"tailwind-variants": "^0.2.0", "tailwind-variants": "^0.2.0",
"usehooks-ts": "^2.15.1", "typescript": "^4.9.5",
"uuid": "^9.0.1", "usehooks-ts": "^2.10.0",
"viem": "^2.7.11", "vertical-stepper-nav": "^1.0.2",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint ."
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": { "devDependencies": {
"@chromatic-com/storybook": "^1.3.3", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@storybook/addon-essentials": "^8.0.10",
"@storybook/addon-interactions": "^8.0.10",
"@storybook/addon-links": "^8.0.10",
"@storybook/addon-onboarding": "^8.0.10",
"@storybook/blocks": "^8.0.10",
"@storybook/react": "^8.0.10",
"@storybook/react-vite": "^8.0.10",
"@storybook/test": "^8.0.10",
"@types/jest": "^27.5.2",
"@types/lodash": "^4.17.0",
"@types/luxon": "^3.3.7", "@types/luxon": "^3.3.7",
"@types/node": "^16.18.68", "@typescript-eslint/eslint-plugin": "^6.13.2",
"@types/react": "^18.2.66", "@typescript-eslint/parser": "^6.13.2",
"@types/react-dom": "^18.2.22", "eslint": "^8.55.0",
"@types/uuid": "^9.0.8", "eslint-config-prettier": "^9.1.0",
"@vitejs/plugin-react": "^4.2.1", "eslint-plugin-prettier": "^5.0.1",
"autoprefixer": "^10.4.19", "eslint-plugin-react": "^7.33.2",
"chromatic": "^11.3.2",
"eslint-plugin-storybook": "^0.8.0",
"postcss": "^8.4.38",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"storybook": "^8.0.10", "tailwindcss": "^3.4.1"
"storybook-addon-remix-react-router": "^3.0.0",
"tailwindcss": "^3.4.3",
"typescript": "^5.3.3",
"vite": "^5.2.0"
} }
} }
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#2d89ef</TileColor>
</tile>
</msapplication>
</browserconfig>
@@ -1,3 +0,0 @@
<svg width="197" height="2" viewBox="0 0 197 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="0.5" y1="1.19141" x2="197" y2="1.19141" stroke="#94A7B8" stroke-dasharray="1 12"/>
</svg>

Before

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Some files were not shown because too many files have changed in this diff Show More