Compare commits
601 Commits
ayungavis/
...
main
Author | SHA1 | Date | |
---|---|---|---|
17640d3133 | |||
bba0b57bed | |||
c34e66aa93 | |||
b61682dd20 | |||
bcb6ac241b | |||
4a9f517d85 | |||
6d1135645b | |||
e5748e768c | |||
9158e3a840 | |||
8bd1e17fd2 | |||
3998b60888 | |||
c5bace0660 | |||
a033a8a7b3 | |||
|
420080b1af | ||
f95a64546d | |||
3214cfc1d4 | |||
07b21a835e | |||
15ba278bbc | |||
364c62783d | |||
b9573474a8 | |||
ea9a56eb65 | |||
05bd766133 | |||
0f18bc978e | |||
519e318190 | |||
63969ae25a | |||
b449c299dc | |||
2a35ec1cd5 | |||
be90fc76c1 | |||
3fa60f3cdf | |||
3d9aedeb7e | |||
096318cf13 | |||
27ef859075 | |||
5152952a45 | |||
ef26f9b39e | |||
d486f44cfe | |||
5c9c7575f2 | |||
59a164f3f8 | |||
bc52b34462 | |||
5aefda1248 | |||
42bdd21089 | |||
|
e5a00016c1 | ||
13b912d318 | |||
059863c4b9 | |||
58906844cc | |||
|
9f0a2ad548 | ||
|
bd10e2cb35 | ||
|
eb32385cf3 | ||
|
aebb20b987 | ||
|
8a2b51952f | ||
|
94f46f9621 | ||
|
e751addcce | ||
|
c01f8fdabf | ||
|
2b60114dab | ||
|
9a1c0e8338 | ||
|
1b038476c7 | ||
|
4a78eb13f6 | ||
|
41bcb2e7d0 | ||
|
f981f1a3f6 | ||
|
dee84f18cb | ||
|
44015d5451 | ||
|
a684743bd6 | ||
|
b12c95b2ff | ||
|
a4d9211ffe | ||
|
af31fac3ee | ||
|
54ae3f429d | ||
|
acfe78bf07 | ||
|
ce1833cb51 | ||
|
f2e59c11fd | ||
|
b261e7e436 | ||
|
934aa1a26b | ||
|
d975390a1b | ||
|
f77323364c | ||
|
c6a78f2116 | ||
|
cff9a5b2ea | ||
|
003b83ba21 | ||
|
82a1c151a8 | ||
|
2ada11f311 | ||
|
6e32d0678a | ||
|
198478f5fa | ||
552dfe783e | |||
|
e2bf5d052c | ||
33323b9bbf | |||
e2a3254563 | |||
8589bf4094 | |||
f8ebdfe7aa | |||
2166b2f800 | |||
cdd8d15e73 | |||
|
dc7b251988 | ||
|
bfb4a3f30b | ||
|
8f7fc888a9 | ||
|
0e9c3a07fd | ||
|
61e3e88a6c | ||
|
7b5ba1a5d0 | ||
|
b35f4033c5 | ||
|
306d3235b3 | ||
|
e148fd8d6b | ||
|
f84e2c0d9d | ||
|
216a5670e6 | ||
|
92016c8837 | ||
|
c6ebcafaac | ||
|
6dfe85cb1a | ||
|
2bb5feeb88 | ||
|
9a74205bd5 | ||
|
7147611842 | ||
|
8533c41c58 | ||
|
2074e08a0c | ||
|
f01bdf2de7 | ||
|
8cb5eadfb2 | ||
|
41666568f5 | ||
|
17cf878168 | ||
|
c72cbce615 | ||
|
a69dd71117 | ||
|
5dc4d28b50 | ||
|
7dce1d66ae | ||
|
46ba6d014d | ||
|
8488cfab83 | ||
|
8376aff7bd | ||
|
583c0b9d26 | ||
|
e12c94e087 | ||
|
879cfdb2bf | ||
|
04b6a84440 | ||
|
41b9ce1096 | ||
|
ad69ebe4a0 | ||
|
ca8863e1d6 | ||
|
0fb1127b96 | ||
|
b34e0783c1 | ||
|
050f404776 | ||
|
2e30a1aae1 | ||
|
7c8e9f2448 | ||
|
065b12e3d9 | ||
|
3668a8edf7 | ||
|
28366ea725 | ||
|
4c3072ed50 | ||
|
6975360d48 | ||
|
671321ef4d | ||
|
d04ad68f18 | ||
|
16df36715a | ||
|
6fdbcf6f46 | ||
|
702cef24b3 | ||
|
d3cf0453e7 | ||
|
d76db4fb96 | ||
|
19545c48bd | ||
|
8d96be625e | ||
|
1c05ba8822 | ||
|
fd7d06b9e2 | ||
|
550ef91968 | ||
|
c710fb5a53 | ||
|
a95f18a66b | ||
|
ccc36fd175 | ||
|
ce7e9174e4 | ||
|
43d7c94cb0 | ||
|
294675c276 | ||
|
2e0b228aa5 | ||
|
907b5ee02d | ||
|
8f456c04f5 | ||
|
29caf510ea | ||
|
a63d0f69ed | ||
|
1aa57aaecc | ||
|
a27331f54f | ||
|
ebe6d35b54 | ||
|
29557d7597 | ||
|
df540e06eb | ||
|
3fe0718532 | ||
|
039753df56 | ||
|
386f40952e | ||
|
e1c4f77ec1 | ||
|
57601e6b4b | ||
|
e9a367db42 | ||
|
6ae04251d2 | ||
|
690fc8d2cb | ||
|
2719b3e385 | ||
|
de6d5c302b | ||
|
dc91fa0d7f | ||
|
571d58d57d | ||
|
947337acb1 | ||
|
f8908c1c06 | ||
|
6e7385b118 | ||
|
abbda18fc7 | ||
|
2d0de785f9 | ||
|
68f1a265f4 | ||
|
9b7a021e8b | ||
|
cdfc72a5a0 | ||
|
47b322c212 | ||
|
e4c39e0955 | ||
|
9f46290ecc | ||
|
b71b7579a6 | ||
|
8aa1362be6 | ||
|
e1d2b789a3 | ||
|
cf161bcba0 | ||
|
57956ec269 | ||
|
fb77433bea | ||
|
146b904556 | ||
|
756cc52e1c | ||
|
ba5f281671 | ||
|
60a66e94bd | ||
|
5f5b0a4d4f | ||
|
5ebdd3003a | ||
|
987643b153 | ||
|
090a9054e4 | ||
|
7dcd6581a7 | ||
|
928958131c | ||
|
327ac62186 | ||
|
37c250873c | ||
|
f7fa47dbc8 | ||
|
1bba82ee86 | ||
|
99cc35459b | ||
|
4b54f2fc2e | ||
|
4906740876 | ||
|
73ff7896f8 | ||
|
67d28910d4 | ||
|
939b1c40e8 | ||
|
c3b048d273 | ||
|
48552310e0 | ||
|
c82e1110d3 | ||
|
b7f29781f2 | ||
|
cf531127c4 | ||
|
44de7bb0f1 | ||
|
ed0de90118 | ||
|
11c8f7b09b | ||
|
3dd62114b4 | ||
|
4f3ea09b79 | ||
|
f4f80903f8 | ||
|
5307eab443 | ||
|
00199424f7 | ||
|
aaadfeab72 | ||
|
3ffac1a019 | ||
|
56a264199b | ||
|
df8bc3784a | ||
|
464b0463af | ||
|
c29134bfd9 | ||
|
9b0ad34831 | ||
|
89b3cb8c31 | ||
|
c52d695ca7 | ||
|
187f75d561 | ||
|
14deecfad6 | ||
|
808b2ad61b | ||
|
775e731f4d | ||
|
813cea055b | ||
|
072e2c20e3 | ||
|
83d5e3d769 | ||
|
9ed52c67c9 | ||
|
f7d6d02b27 | ||
|
d7dc9a07f9 | ||
|
6cbc87a7d1 | ||
|
ddb54de49b | ||
|
cf9fd04272 | ||
|
535c37d0b4 | ||
|
46476bef28 | ||
8b39f664ad | |||
5f4be30799 | |||
b53e12b94b | |||
f290b5c0b5 | |||
4fa6f418ba | |||
|
328da7fdc8 | ||
|
8210512eea | ||
|
cd2dce2404 | ||
|
c2158510d9 | ||
|
096fd04a22 | ||
|
a8d93732ce | ||
|
953c3fc10b | ||
|
3cbea57294 | ||
|
27f5c57c37 | ||
|
d1c3249b98 | ||
|
d34a5f29cd | ||
|
4a14681753 | ||
|
aea6bfde54 | ||
|
4aac93b504 | ||
|
e6b68dcce4 | ||
|
d2daed4cac | ||
|
c395be82b5 | ||
|
748ca507da | ||
|
bc210fdb0f | ||
|
bace4a6ce6 | ||
|
0d36cc1b6d | ||
|
1ed4ee979c | ||
72f1abcdf6 | |||
|
57add027ab | ||
|
7ad02d27bf | ||
|
b17e9df1e4 | ||
|
baadc507e7 | ||
|
6316aa852b | ||
|
2274e8d145 | ||
|
f8d706233e | ||
cc8f9527da | |||
|
47231a6eab | ||
|
4774074b67 | ||
|
0da7c3541e | ||
|
b7b4ab1f14 | ||
|
ea6ad52a73 | ||
|
f38dfb5604 | ||
351db16336 | |||
|
78f04c3669 | ||
|
44a6a8902f | ||
|
4fcf8e92b7 | ||
|
193dbb058a | ||
|
8834de893e | ||
|
9b3ac4654f | ||
|
e4fdae3329 | ||
|
4c936b1eb7 | ||
|
f5807c1126 | ||
|
c1696fbf48 | ||
|
45e8e9a7f4 | ||
|
28740ffbee | ||
|
97289d85a3 | ||
|
f3ce0d0621 | ||
|
c2417a9daa | ||
|
d50a318e16 | ||
|
3674750011 | ||
|
102c861617 | ||
|
56e9be59ad | ||
|
300b8e4b5e | ||
|
296e149391 | ||
|
cbda1cc652 | ||
|
16e7b22507 | ||
|
93fb696d5c | ||
|
32305cedbc | ||
|
519618b456 | ||
|
4057480df2 | ||
|
de197759de | ||
|
65b66fe383 | ||
|
c6ea3a8f53 | ||
|
6d861c71cc | ||
|
1d96f6430f | ||
|
b922e632bf | ||
|
b1f318bbbd | ||
|
667deb78fb | ||
|
095008867f | ||
|
41033c5241 | ||
|
cdb995205a | ||
|
3a84832da5 | ||
|
3fdc0b2dff | ||
|
89da03cec6 | ||
|
9b6cd9baae | ||
|
658cc0b6b2 | ||
|
be8f0bc4ad | ||
|
2a8fc30979 | ||
|
4074dd4001 | ||
|
028831f806 | ||
|
646099707c | ||
|
6b0548ec47 | ||
|
2ed07a6987 | ||
|
e9ab034625 | ||
|
02500b3cba | ||
|
4c8d0e2436 | ||
|
efa74898af | ||
|
532732943e | ||
|
be4900e63f | ||
|
9e4e203f5f | ||
|
d812cdf05f | ||
|
b605c1bc5b | ||
|
8d3ef369bb | ||
|
7aac8cdcef | ||
|
6c31113bca | ||
|
30ecd41975 | ||
|
410375f8c7 | ||
|
226d02ea59 | ||
|
ca64d00355 | ||
|
287fe360d3 | ||
|
f23c757de9 | ||
|
3737550f7f | ||
|
274762cbfe | ||
|
90295fd620 | ||
|
b75957929a | ||
|
daffcf1203 | ||
|
eccf78afdb | ||
|
0901036f71 | ||
|
8d3ef3bafc | ||
|
b6e02fb19d | ||
|
462d247a86 | ||
|
80097a32ac | ||
|
a80d40156f | ||
|
ced50bf7f2 | ||
|
90b583aa19 | ||
|
48e3581322 | ||
|
1ddc3b81c7 | ||
|
5a86abd133 | ||
|
6ddf44cddd | ||
|
b03200c256 | ||
|
1648deb64f | ||
|
621ca8926e | ||
|
8c0162f9f3 | ||
|
e05e2c63c2 | ||
|
3b1f03bcb6 | ||
|
80fee1f585 | ||
|
d50f559a07 | ||
|
46d44378c5 | ||
|
3e42899f2e | ||
|
75a048c7a9 | ||
|
b5f50fe16f | ||
|
aa4094669d | ||
|
33fbdf60ae | ||
|
abe1401a6d | ||
|
2182beca22 | ||
|
96f519d5e3 | ||
|
f6db1e119b | ||
|
c079a9c336 | ||
|
f9ac778e47 | ||
|
e8ae417772 | ||
|
18dde52c9a | ||
|
d9bae720d7 | ||
|
e9f32ff668 | ||
|
8a1e84386a | ||
|
3335c26f82 | ||
|
0a2686d5ec | ||
|
c1da92bc99 | ||
|
add2559860 | ||
|
460f8a80b8 | ||
|
f5296e9b8d | ||
|
0aeea36dbd | ||
|
8f0c39022d | ||
|
6c42a22972 | ||
|
adb64f2db1 | ||
|
64e3aa5b25 | ||
|
dc6f436409 | ||
|
691edf3508 | ||
|
4fc654f763 | ||
|
fe599f97a3 | ||
|
be0ea904ee | ||
|
8114b10932 | ||
|
0ee27df25e | ||
|
893a499be5 | ||
|
596d8eb326 | ||
|
e9d38e0d3b | ||
|
ab77ce681a | ||
|
5be6e14db9 | ||
|
409b654f9b | ||
|
4a86952810 | ||
|
207b32ce65 | ||
|
6e653774be | ||
|
0b64ccc364 | ||
|
a8ad6c6eec | ||
65f64a3dcd | |||
d64cf46d89 | |||
1ff5ab3dfd | |||
|
d9392c095d | ||
|
2cd1865d19 | ||
|
a7810a34c9 | ||
|
ff9b08e91d | ||
94a9bf88a9 | |||
|
dd5f93050d | ||
|
11aeda9453 | ||
|
dfa38a4cba | ||
|
638e31f7fe | ||
|
5946755749 | ||
|
4532a962d8 | ||
|
83fec1d273 | ||
|
6db5871868 | ||
|
e0c5895e9c | ||
|
630af612a2 | ||
|
87307381cb | ||
|
6a108c1a1b | ||
|
aa5507f309 | ||
|
91a80cac8a | ||
|
ee397d0e6e | ||
|
98ec1915ca | ||
|
f08081932c | ||
|
fe427a9fb8 | ||
|
a5b4103515 | ||
|
f77f7c120a | ||
|
22f3cbafb7 | ||
|
30c50a3eec | ||
|
0381fdbbed | ||
|
2f466d4fbb | ||
|
ee35e64f69 | ||
|
0fb378386a | ||
|
563059e887 | ||
|
075cec58e5 | ||
|
d52a34da35 | ||
|
eb4ccfbc9c | ||
|
769593913e | ||
|
eda2cc76ed | ||
|
99eb514306 | ||
|
f34eb48d1e | ||
|
acf5aab26f | ||
|
6b6582c287 | ||
|
b43ee3b7bb | ||
|
b1bf47d104 | ||
|
8ee61c0c85 | ||
|
33aa75e341 | ||
|
a238610522 | ||
|
bd4a51c830 | ||
|
4a87c100d6 | ||
|
3493d735b9 | ||
|
a324d32ebf | ||
|
4519494be8 | ||
|
319822c8e6 | ||
|
c5273ff530 | ||
|
2585418ed6 | ||
|
62734308fc | ||
|
89306ddcc7 | ||
|
3b67396c43 | ||
|
3535bd3a58 | ||
|
282001c317 | ||
|
2f62d086de | ||
|
4c544e8d15 | ||
|
a02ee13f9d | ||
|
e32a51a771 | ||
|
513ca69d01 | ||
|
e34b8d6a2e | ||
|
f4d5b77784 | ||
|
399b80ce3b | ||
|
fdaec2da94 | ||
|
13fc92bf0e | ||
|
8a8ee1d6ae | ||
|
5680961964 | ||
|
aa0ca8dd21 | ||
|
8280f4c7f7 | ||
ab72bbb03f | |||
cc4e5fd627 | |||
|
195471b888 | ||
|
c731dd308c | ||
|
8ce531274d | ||
|
6c38094093 | ||
|
95285d30dd | ||
|
61c8846400 | ||
|
d73c3c7daf | ||
|
effa0bbf14 | ||
|
10a875d6e8 | ||
|
284eb1403a | ||
|
538e89e3c9 | ||
|
b88752da69 | ||
|
48c569c371 | ||
|
549dc5af10 | ||
|
012beaad57 | ||
|
81f27442fb | ||
|
bdb948f69e | ||
|
2442e92395 | ||
|
dcf271a01b | ||
|
48fd953c60 | ||
|
5eff273abb | ||
|
f9af066333 | ||
|
04460a24ac | ||
|
ccc4589033 | ||
|
cf2f9a88e1 | ||
|
50749055bf | ||
|
ef7c673698 | ||
|
7f11d8818d | ||
|
ece9a2ecec | ||
|
cb614c8d8f | ||
|
dbb928cd2f | ||
|
cfd129fb96 | ||
|
efc10d3d7e | ||
|
042aff2f87 | ||
b48afed24f | |||
|
0dfecd024d | ||
6126c03eee | |||
35be62b28e | |||
|
9db87e6be3 | ||
353fd0f621 | |||
|
5a5756bcf2 | ||
|
22233d95f8 | ||
|
12fba5ee0c | ||
|
9b6c777f5f | ||
|
3718e260f5 | ||
|
05e1ac6b02 | ||
|
55848b1dd1 | ||
|
42bf1534fb | ||
9acb9daacc | |||
f7c45ca045 | |||
ef0eac8293 | |||
|
7d1810ebd9 | ||
|
d2ca4df35a | ||
|
30bbe4d766 | ||
|
0e7343da6f | ||
|
8952393b31 | ||
|
32918b7930 | ||
|
596889b5e2 | ||
|
fb932eeb04 | ||
|
200ea3ca7b | ||
|
ba87c6f22a | ||
|
4d646e1ac0 | ||
|
5be29a9745 | ||
|
fffc370d40 | ||
|
1cd60d84dd | ||
a846531e43 | |||
|
c8a153ad27 | ||
|
bb7a88f7e9 | ||
|
675112079c | ||
|
cb928c3360 | ||
|
5836779403 | ||
|
028dd35db1 | ||
|
ef9f4df28a | ||
6b17dce2ae | |||
|
496404195c | ||
|
9dec48d67c | ||
|
267b52a352 | ||
|
fa96b6e734 | ||
|
22c581dd33 | ||
|
d3013719e6 | ||
|
98a4d7be07 | ||
|
3928e2610f | ||
|
075cc05db5 | ||
e816c596ca | |||
|
6df094bf2e | ||
4d0f2ca893 | |||
8ca55cd888 | |||
fc240c93d8 | |||
a45fb4c617 | |||
c3d1b4f3eb | |||
ce55fe62d8 | |||
b007286f4a |
61
.gitea/workflows/deploy-frontend.yaml
Normal file
61
.gitea/workflows/deploy-frontend.yaml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
name: Deploy Snowball frontend
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY_USER_KEY: ${{ secrets.REGISTRY_USER_KEY }}
|
||||||
|
REGISTRY_BOND_ID: ${{ secrets.REGISTRY_BOND_ID }}
|
||||||
|
DEPLOYER_LRN: lrn://vaasl-provider/deployers/webapp-deployer-api.apps.vaasl.io
|
||||||
|
AUTHORITY: laconic-deploy
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Download yarn
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o /usr/local/bin/yarn https://github.com/yarnpkg/yarn/releases/download/v1.22.21/yarn-1.22.21.js
|
||||||
|
chmod +x /usr/local/bin/yarn
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
- name: Set up environment
|
||||||
|
run: |
|
||||||
|
# Create a .env file with the necessary variables
|
||||||
|
echo "REGISTRY_BOND_ID=$REGISTRY_BOND_ID" > packages/deployer/.env
|
||||||
|
echo "DEPLOYER_LRN=$DEPLOYER_LRN" >> packages/deployer/.env
|
||||||
|
echo "AUTHORITY=$AUTHORITY" >> packages/deployer/.env
|
||||||
|
|
||||||
|
# Create a config file with necessary endpoints and secrets
|
||||||
|
cat > packages/deployer/config.yml <<EOF
|
||||||
|
services:
|
||||||
|
registry:
|
||||||
|
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||||
|
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||||
|
userKey: $REGISTRY_USER_KEY
|
||||||
|
bondId: $REGISTRY_BOND_ID
|
||||||
|
chainId: laconic-testnet-2
|
||||||
|
gasPrice: 0.001alnt
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Run deploy script
|
||||||
|
run: |
|
||||||
|
cd packages/deployer
|
||||||
|
./deploy-frontend.sh
|
30
.gitea/workflows/lint.yaml
Normal file
30
.gitea/workflows/lint.yaml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- staging
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20.x]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Download yarn
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o /usr/local/bin/yarn https://github.com/yarnpkg/yarn/releases/download/v1.22.21/yarn-1.22.21.js
|
||||||
|
chmod +x /usr/local/bin/yarn
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- run: yarn
|
||||||
|
- name: Build libs
|
||||||
|
run: yarn workspace gql-client run build
|
||||||
|
- name: Linter check
|
||||||
|
run: yarn lint
|
2
.github/workflows/lint.yaml
vendored
2
.github/workflows/lint.yaml
vendored
@ -19,5 +19,7 @@ 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
|
||||||
|
39
.github/workflows/test-app-deployment.yaml
vendored
Normal file
39
.github/workflows/test-app-deployment.yaml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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 }}
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -1 +1,12 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
yarn-error.log
|
||||||
|
.yarnrc.yml
|
||||||
|
.yarn/
|
||||||
|
.yarnrc
|
||||||
|
|
||||||
|
packages/backend/environments/local.toml
|
||||||
|
packages/backend/dev/
|
||||||
|
packages/frontend/dist/
|
||||||
|
|
||||||
|
# ignore all .DS_Store files
|
||||||
|
**/.DS_Store
|
||||||
|
1
.node-version
Normal file
1
.node-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
v20.12.1
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
// IntelliSense for taiwind variants
|
// IntelliSense for taiwind variants
|
||||||
"tailwindCSS.experimental.classRegex": [
|
"tailwindCSS.experimental.classRegex": [
|
||||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
"tv\\('([^)]*)\\')",
|
||||||
|
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
155
README.md
155
README.md
@ -1,150 +1,23 @@
|
|||||||
# snowballtools
|
# snowballtools-base
|
||||||
|
|
||||||
## Setup
|
This is a [yarn workspace](https://yarnpkg.com/features/workspaces) monorepo for the dashboard.
|
||||||
|
|
||||||
- Clone the `snowballtools` repo
|
## Getting Started
|
||||||
|
|
||||||
```bash
|
### Install dependencies
|
||||||
git clone git@github.com:snowball-tools/snowballtools-base.git
|
|
||||||
```
|
|
||||||
|
|
||||||
- In root of the repo, install depedencies
|
In the root of the project, run:
|
||||||
|
|
||||||
```bash
|
```zsh
|
||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
- Build packages
|
### Build backend
|
||||||
|
|
||||||
```bash
|
```zsh
|
||||||
yarn build --ignore frontend
|
yarn build --ignore frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
## Backend
|
### Environment variables, running the development server, and deployment
|
||||||
|
|
||||||
- Change directory to `packages/backend`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd packages/backend
|
|
||||||
```
|
|
||||||
|
|
||||||
- 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
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
|
Follow the instructions in the README.md files of the [backend](packages/backend/README.md) and [frontend](packages/frontend/README.md) packages.
|
||||||
|
35
build-webapp.sh
Executable file
35
build-webapp.sh
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/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_GITHUB_NEXT_APP_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_next_app_templaterepo'
|
||||||
|
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
|
||||||
|
VITE_LACONICD_CHAIN_ID = 'LACONIC_HOSTED_CONFIG_laconicd_chain_id'
|
||||||
|
VITE_WALLET_IFRAME_URL = 'LACONIC_HOSTED_CONFIG_wallet_iframe_url'
|
||||||
|
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_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'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
yarn || exit 1
|
||||||
|
yarn build --ignore backend || exit 1
|
||||||
|
|
||||||
|
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
||||||
|
echo "Missing output directory: $OUTPUT_DIR" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$OUTPUT_DIR" "$DEST_DIR"
|
@ -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",
|
||||||
"depcheck": "^1.4.2"
|
"patch-package": "^8.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"build": "lerna run build --stream",
|
"build": "lerna run build --stream",
|
||||||
"lint": "lerna run lint --stream -- --max-warnings=0"
|
"lint": "lerna run lint --stream"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -25,6 +25,9 @@
|
|||||||
"allowArgumentsExplicitlyTypedAsAny": true
|
"allowArgumentsExplicitlyTypedAsAny": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ "ignoreRestSiblings": true }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
packages/backend/.gitignore
vendored
1
packages/backend/.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
db
|
db
|
||||||
dist
|
dist
|
||||||
|
environments/local.toml
|
1
packages/backend/.node-version
Normal file
1
packages/backend/.node-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
v20.12.1
|
@ -1 +1,76 @@
|
|||||||
# Backend for Snowball Tools
|
# backend
|
||||||
|
|
||||||
|
This backend is a [node.js](https://nodejs.org/) [express.js](https://expressjs.com/) [apollo server](https://www.apollographql.com/docs/apollo-server/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
|
||||||
|
In the root of the project, run:
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build backend
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
yarn build --ignore frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
#### Local
|
||||||
|
|
||||||
|
Copy the `environments/local.toml.example` file to `environments/local.toml`:
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
cp environments/local.toml.example environments/local.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Staging environment variables
|
||||||
|
|
||||||
|
In the deployment repository, update staging [staging/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/staging/configmaps/config/prod.toml)
|
||||||
|
|
||||||
|
#### Production environment variables
|
||||||
|
|
||||||
|
In the deployment repository, update production [production/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/production/configmaps/config/prod.toml)
|
||||||
|
|
||||||
|
### Run development server
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments):
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Staging
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
echo trigger >> .gitea/workflows/triggers/staging-deploy
|
||||||
|
git commit -a -m "Deploy v0.0.8" # replace with version number
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
echo trigger >> .gitea/workflows/triggers/production-deploy
|
||||||
|
git commit -a -m "Deploy v0.0.8" # replace with version number
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment status
|
||||||
|
|
||||||
|
Dumb for now
|
||||||
|
|
||||||
|
- [Staging](https://snowballtools-base-api.staging.apps.snowballtools.com/staging/version)
|
||||||
|
- [Production](https://snowballtools-base-api.apps.snowballtools.com/staging/version)
|
||||||
|
|
||||||
|
Update version number manually in [routes/staging.ts](/packages/backend/src/routes/staging.ts)
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
[server]
|
|
||||||
host = "127.0.0.1"
|
|
||||||
port = 8000
|
|
||||||
gqlPath = "/graphql"
|
|
||||||
|
|
||||||
[database]
|
|
||||||
dbPath = "db/snowball"
|
|
||||||
|
|
||||||
[gitHub]
|
|
||||||
webhookUrl = ""
|
|
||||||
[gitHub.oAuth]
|
|
||||||
clientId = ""
|
|
||||||
clientSecret = ""
|
|
||||||
|
|
||||||
[registryConfig]
|
|
||||||
fetchDeploymentRecordDelay = 5000
|
|
||||||
restEndpoint = "http://localhost:1317"
|
|
||||||
gqlEndpoint = "http://localhost:9473/api"
|
|
||||||
chainId = "laconic_9000-1"
|
|
||||||
privateKey = ""
|
|
||||||
bondId = ""
|
|
||||||
[registryConfig.fee]
|
|
||||||
amount = "200000"
|
|
||||||
denom = "aphoton"
|
|
||||||
gas = "550000"
|
|
43
packages/backend/environments/local.toml.example
Normal file
43
packages/backend/environments/local.toml.example
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
[server]
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 8000
|
||||||
|
gqlPath = "/graphql"
|
||||||
|
[server.session]
|
||||||
|
secret = ""
|
||||||
|
# Frontend webapp URL origin
|
||||||
|
appOriginUrl = "http://localhost:3000"
|
||||||
|
# Set to true if server running behind proxy
|
||||||
|
trustProxy = false
|
||||||
|
# Backend URL hostname
|
||||||
|
domain = "localhost"
|
||||||
|
|
||||||
|
[database]
|
||||||
|
dbPath = "db/snowball"
|
||||||
|
|
||||||
|
[gitHub]
|
||||||
|
webhookUrl = ""
|
||||||
|
[gitHub.oAuth]
|
||||||
|
clientId = ""
|
||||||
|
clientSecret = ""
|
||||||
|
|
||||||
|
[registryConfig]
|
||||||
|
fetchDeploymentRecordDelay = 5000
|
||||||
|
checkAuctionStatusDelay = 5000
|
||||||
|
restEndpoint = "http://localhost:1317"
|
||||||
|
gqlEndpoint = "http://localhost:9473/api"
|
||||||
|
chainId = "laconic_9000-1"
|
||||||
|
privateKey = ""
|
||||||
|
bondId = ""
|
||||||
|
authority = ""
|
||||||
|
[registryConfig.fee]
|
||||||
|
gas = ""
|
||||||
|
fees = ""
|
||||||
|
gasPrice = "1alnt"
|
||||||
|
|
||||||
|
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions
|
||||||
|
[auction]
|
||||||
|
commitFee = "100000"
|
||||||
|
commitsDuration = "120s"
|
||||||
|
revealFee = "100000"
|
||||||
|
revealsDuration = "120s"
|
||||||
|
denom = "alnt"
|
@ -1,19 +1,26 @@
|
|||||||
{
|
{
|
||||||
"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",
|
"@cerc-io/registry-sdk": "^0.2.11",
|
||||||
"@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",
|
||||||
|
"@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-async-errors": "^3.1.1",
|
||||||
|
"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",
|
||||||
@ -33,29 +40,21 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
|
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 {
|
||||||
@ -13,7 +24,7 @@ export interface GitHubConfig {
|
|||||||
oAuth: {
|
oAuth: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegistryConfig {
|
export interface RegistryConfig {
|
||||||
@ -23,11 +34,21 @@ export interface RegistryConfig {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
bondId: string;
|
bondId: string;
|
||||||
fetchDeploymentRecordDelay: number;
|
fetchDeploymentRecordDelay: number;
|
||||||
|
checkAuctionStatusDelay: number;
|
||||||
|
authority: string;
|
||||||
fee: {
|
fee: {
|
||||||
amount: string;
|
|
||||||
denom: string;
|
|
||||||
gas: string;
|
gas: string;
|
||||||
}
|
fees: string;
|
||||||
|
gasPrice: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuctionConfig {
|
||||||
|
commitFee: string;
|
||||||
|
commitsDuration: string;
|
||||||
|
revealFee: string;
|
||||||
|
revealsDuration: string;
|
||||||
|
denom: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
@ -35,4 +56,11 @@ export interface Config {
|
|||||||
database: DatabaseConfig;
|
database: DatabaseConfig;
|
||||||
gitHub: GitHubConfig;
|
gitHub: GitHubConfig;
|
||||||
registryConfig: RegistryConfig;
|
registryConfig: RegistryConfig;
|
||||||
|
auction: AuctionConfig;
|
||||||
|
turnkey: {
|
||||||
|
apiBaseUrl: string;
|
||||||
|
apiPublicKey: string;
|
||||||
|
apiPrivateKey: string;
|
||||||
|
defaultOrganizationId: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
export const DEFAULT_CONFIG_FILE_PATH = 'environments/local.toml';
|
import process from 'process';
|
||||||
|
|
||||||
|
export const DEFAULT_CONFIG_FILE_PATH =
|
||||||
|
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
|
||||||
|
|
||||||
export const DEFAULT_GQL_PATH = '/graphql';
|
export const DEFAULT_GQL_PATH = '/graphql';
|
||||||
|
|
||||||
// Note: temporary hardcoded user, later to be derived from auth token
|
|
||||||
export const USER_ID = '59f4355d-9549-4aac-9b54-eeefceeabef0';
|
|
||||||
|
|
||||||
export const PROJECT_DOMAIN = 'snowball.xyz';
|
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm';
|
import {
|
||||||
|
DataSource,
|
||||||
|
DeepPartial,
|
||||||
|
FindManyOptions,
|
||||||
|
FindOneOptions,
|
||||||
|
FindOptionsWhere,
|
||||||
|
IsNull,
|
||||||
|
Not
|
||||||
|
} 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';
|
||||||
@ -13,7 +21,11 @@ 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 { PROJECT_DOMAIN } from './constants';
|
import { getEntities, loadAndSaveData } from './utils';
|
||||||
|
import { UserOrganization } from './entity/UserOrganization';
|
||||||
|
import { Deployer } from './entity/Deployer';
|
||||||
|
|
||||||
|
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
||||||
|
|
||||||
const log = debug('snowball:database');
|
const log = debug('snowball:database');
|
||||||
|
|
||||||
@ -23,7 +35,7 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
|
|||||||
export class Database {
|
export class Database {
|
||||||
private dataSource: DataSource;
|
private dataSource: DataSource;
|
||||||
|
|
||||||
constructor ({ dbPath }: DatabaseConfig) {
|
constructor({ dbPath }: DatabaseConfig) {
|
||||||
this.dataSource = new DataSource({
|
this.dataSource = new DataSource({
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
database: dbPath,
|
database: dbPath,
|
||||||
@ -33,41 +45,73 @@ export class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async init (): Promise<void> {
|
async init(): Promise<void> {
|
||||||
await this.dataSource.initialize();
|
await this.dataSource.initialize();
|
||||||
log('database initialized');
|
log('database initialized');
|
||||||
|
|
||||||
|
let organizations = await this.getOrganizations({});
|
||||||
|
|
||||||
|
// Load an organization if none exist
|
||||||
|
if (!organizations.length) {
|
||||||
|
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||||
|
organizations = await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser (options: FindOneOptions<User>): Promise<User | null> {
|
// Hotfix for updating old DB data
|
||||||
|
if (organizations[0].slug === 'snowball-tools-1') {
|
||||||
|
const [orgEntity] = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||||
|
|
||||||
|
await this.updateOrganization(
|
||||||
|
organizations[0].id,
|
||||||
|
{
|
||||||
|
slug: orgEntity.slug as string,
|
||||||
|
name: orgEntity.name as string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(options: FindOneOptions<User>): Promise<User | null> {
|
||||||
const userRepository = this.dataSource.getRepository(User);
|
const userRepository = this.dataSource.getRepository(User);
|
||||||
const user = await userRepository.findOne(options);
|
const user = await userRepository.findOne(options);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addUser (data: DeepPartial<User>): Promise<User> {
|
async addUser(data: DeepPartial<User>): Promise<User> {
|
||||||
const userRepository = this.dataSource.getRepository(User);
|
const userRepository = this.dataSource.getRepository(User);
|
||||||
const user = await userRepository.save(data);
|
const user = await userRepository.save(data);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser (userId: string, data: DeepPartial<User>): Promise<boolean> {
|
async updateUser(user: User, data: DeepPartial<User>): Promise<boolean> {
|
||||||
const userRepository = this.dataSource.getRepository(User);
|
const userRepository = this.dataSource.getRepository(User);
|
||||||
const updateResult = await userRepository.update({ id: userId }, data);
|
const updateResult = await userRepository.update({ id: user.id }, data);
|
||||||
assert(updateResult.affected);
|
assert(updateResult.affected);
|
||||||
|
|
||||||
return updateResult.affected > 0;
|
return updateResult.affected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> {
|
async getOrganizations(
|
||||||
|
options: FindManyOptions<Organization>
|
||||||
|
): Promise<Organization[]> {
|
||||||
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
|
const organizations = await organizationRepository.find(options);
|
||||||
|
|
||||||
|
return organizations;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganization(
|
||||||
|
options: FindOneOptions<Organization>
|
||||||
|
): Promise<Organization | null> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
const organization = await organizationRepository.findOne(options);
|
const organization = await organizationRepository.findOne(options);
|
||||||
|
|
||||||
return organization;
|
return organization;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
async getOrganizationsByUserId(userId: string): Promise<Organization[]> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
|
|
||||||
const userOrgs = await organizationRepository.find({
|
const userOrgs = await organizationRepository.find({
|
||||||
@ -83,22 +127,43 @@ export class Database {
|
|||||||
return userOrgs;
|
return userOrgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects (options: FindManyOptions<Project>): Promise<Project[]> {
|
async addUserOrganization(data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
|
||||||
|
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
|
||||||
|
const newUserOrganization = await userOrganizationRepository.save(data);
|
||||||
|
|
||||||
|
return newUserOrganization;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOrganization(organizationId: string, data: DeepPartial<Organization>): Promise<boolean> {
|
||||||
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
|
const updateResult = await organizationRepository.update({ id: organizationId }, data);
|
||||||
|
assert(updateResult.affected);
|
||||||
|
|
||||||
|
return updateResult.affected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjects(options: FindManyOptions<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);
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectById (projectId: string): Promise<Project | null> {
|
async getProjectById(projectId: string): Promise<Project | null> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const project = await projectRepository
|
const project = await projectRepository
|
||||||
.createQueryBuilder('project')
|
.createQueryBuilder('project')
|
||||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
.leftJoinAndSelect(
|
||||||
|
'project.deployments',
|
||||||
|
'deployments',
|
||||||
|
'deployments.isCurrent = true'
|
||||||
|
)
|
||||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||||
|
.leftJoinAndSelect('deployments.deployer', 'deployer')
|
||||||
.leftJoinAndSelect('project.owner', 'owner')
|
.leftJoinAndSelect('project.owner', 'owner')
|
||||||
|
.leftJoinAndSelect('project.deployers', 'deployers')
|
||||||
.leftJoinAndSelect('project.organization', 'organization')
|
.leftJoinAndSelect('project.organization', 'organization')
|
||||||
.where('project.id = :projectId', {
|
.where('project.id = :projectId', {
|
||||||
projectId
|
projectId
|
||||||
@ -108,19 +173,47 @@ export class Database {
|
|||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsInOrganization (userId: string, organizationSlug: string): Promise<Project[]> {
|
async allProjectsWithoutDeployments(): Promise<Project[]> {
|
||||||
|
const allProjects = await this.getProjects({
|
||||||
|
where: {
|
||||||
|
auctionId: Not(IsNull()),
|
||||||
|
},
|
||||||
|
relations: ['deployments'],
|
||||||
|
withDeleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const projects = allProjects.filter(project => {
|
||||||
|
if (project.deletedAt !== null) return false;
|
||||||
|
|
||||||
|
return project.deployments.length === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectsInOrganization(
|
||||||
|
userId: string,
|
||||||
|
organizationSlug: string
|
||||||
|
): Promise<Project[]> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const projects = await projectRepository
|
const projects = await projectRepository
|
||||||
.createQueryBuilder('project')
|
.createQueryBuilder('project')
|
||||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
.leftJoinAndSelect(
|
||||||
|
'project.deployments',
|
||||||
|
'deployments',
|
||||||
|
'deployments.isCurrent = true'
|
||||||
|
)
|
||||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||||
.leftJoin('project.projectMembers', 'projectMembers')
|
.leftJoin('project.projectMembers', 'projectMembers')
|
||||||
.leftJoin('project.organization', 'organization')
|
.leftJoin('project.organization', 'organization')
|
||||||
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', {
|
.where(
|
||||||
|
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug',
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
organizationSlug
|
organizationSlug
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
@ -129,19 +222,22 @@ export class Database {
|
|||||||
/**
|
/**
|
||||||
* Get deployments with specified filter
|
* Get deployments with specified filter
|
||||||
*/
|
*/
|
||||||
async getDeployments (options: FindManyOptions<Deployment>): Promise<Deployment[]> {
|
async getDeployments(
|
||||||
|
options: FindManyOptions<Deployment>
|
||||||
|
): Promise<Deployment[]> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const deployments = await deploymentRepository.find(options);
|
const deployments = await deploymentRepository.find(options);
|
||||||
|
|
||||||
return deployments;
|
return deployments;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
|
async getDeploymentsByProjectId(projectId: string): Promise<Deployment[]> {
|
||||||
return this.getDeployments({
|
return this.getDeployments({
|
||||||
relations: {
|
relations: {
|
||||||
project: true,
|
project: true,
|
||||||
domain: true,
|
domain: true,
|
||||||
createdBy: true
|
createdBy: true,
|
||||||
|
deployer: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
project: {
|
project: {
|
||||||
@ -154,21 +250,23 @@ export class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> {
|
async getDeployment(
|
||||||
|
options: FindOneOptions<Deployment>
|
||||||
|
): Promise<Deployment | null> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const deployment = await deploymentRepository.findOne(options);
|
const deployment = await deploymentRepository.findOne(options);
|
||||||
|
|
||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomains (options: FindManyOptions<Domain>): Promise<Domain[]> {
|
async getDomains(options: FindManyOptions<Domain>): Promise<Domain[]> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const domains = await domainRepository.find(options);
|
const domains = await domainRepository.find(options);
|
||||||
|
|
||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDeployment (data: DeepPartial<Deployment>): Promise<Deployment> {
|
async addDeployment(data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
|
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
@ -182,8 +280,11 @@ export class Database {
|
|||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
async getProjectMembersByProjectId(
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
projectId: string
|
||||||
|
): Promise<ProjectMember[]> {
|
||||||
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const projectMembers = await projectMemberRepository.find({
|
const projectMembers = await projectMemberRepository.find({
|
||||||
relations: {
|
relations: {
|
||||||
@ -200,8 +301,12 @@ export class Database {
|
|||||||
return projectMembers;
|
return projectMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere<EnvironmentVariable>): Promise<EnvironmentVariable[]> {
|
async getEnvironmentVariablesByProjectId(
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
projectId: string,
|
||||||
|
filter?: FindOptionsWhere<EnvironmentVariable>
|
||||||
|
): Promise<EnvironmentVariable[]> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
|
||||||
const environmentVariables = await environmentVariableRepository.find({
|
const environmentVariables = await environmentVariableRepository.find({
|
||||||
where: {
|
where: {
|
||||||
@ -215,10 +320,13 @@ export class Database {
|
|||||||
return environmentVariables;
|
return environmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
async removeProjectMemberById(projectMemberId: string): Promise<boolean> {
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const deleteResult = await projectMemberRepository.delete({ id: projectMemberId });
|
const deleteResult = await projectMemberRepository.delete({
|
||||||
|
id: projectMemberId
|
||||||
|
});
|
||||||
|
|
||||||
if (deleteResult.affected) {
|
if (deleteResult.affected) {
|
||||||
return deleteResult.affected > 0;
|
return deleteResult.affected > 0;
|
||||||
@ -227,37 +335,63 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> {
|
async updateProjectMemberById(
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
projectMemberId: string,
|
||||||
const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data);
|
data: DeepPartial<ProjectMember>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
const updateResult = await projectMemberRepository.update(
|
||||||
|
{ id: projectMemberId },
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> {
|
async addProjectMember(
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
data: DeepPartial<ProjectMember>
|
||||||
|
): Promise<ProjectMember> {
|
||||||
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
const newProjectMember = await projectMemberRepository.save(data);
|
const newProjectMember = await projectMemberRepository.save(data);
|
||||||
|
|
||||||
return newProjectMember;
|
return newProjectMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> {
|
async addEnvironmentVariables(
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
data: DeepPartial<EnvironmentVariable>[]
|
||||||
const savedEnvironmentVariables = await environmentVariableRepository.save(data);
|
): Promise<EnvironmentVariable[]> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
const savedEnvironmentVariables =
|
||||||
|
await environmentVariableRepository.save(data);
|
||||||
|
|
||||||
return savedEnvironmentVariables;
|
return savedEnvironmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
async updateEnvironmentVariable(
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
environmentVariableId: string,
|
||||||
const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data);
|
data: DeepPartial<EnvironmentVariable>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
const updateResult = await environmentVariableRepository.update(
|
||||||
|
{ id: environmentVariableId },
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
async deleteEnvironmentVariable(
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
environmentVariableId: string
|
||||||
const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId });
|
): Promise<boolean> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
const deleteResult = await environmentVariableRepository.delete({
|
||||||
|
id: environmentVariableId
|
||||||
|
});
|
||||||
|
|
||||||
if (deleteResult.affected) {
|
if (deleteResult.affected) {
|
||||||
return deleteResult.affected > 0;
|
return deleteResult.affected > 0;
|
||||||
@ -266,8 +400,9 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
async getProjectMemberById(projectMemberId: string): Promise<ProjectMember> {
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const projectMemberWithProject = await projectMemberRepository.find({
|
const projectMemberWithProject = await projectMemberRepository.find({
|
||||||
relations: {
|
relations: {
|
||||||
@ -279,8 +414,7 @@ export class Database {
|
|||||||
where: {
|
where: {
|
||||||
id: projectMemberId
|
id: projectMemberId
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (projectMemberWithProject.length === 0) {
|
if (projectMemberWithProject.length === 0) {
|
||||||
throw new Error('Member does not exist');
|
throw new Error('Member does not exist');
|
||||||
@ -289,34 +423,49 @@ export class Database {
|
|||||||
return projectMemberWithProject[0];
|
return projectMemberWithProject[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsBySearchText (userId: string, searchText: string): Promise<Project[]> {
|
async getProjectsBySearchText(
|
||||||
|
userId: string,
|
||||||
|
searchText: string
|
||||||
|
): Promise<Project[]> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const projects = await projectRepository
|
const projects = await projectRepository
|
||||||
.createQueryBuilder('project')
|
.createQueryBuilder('project')
|
||||||
.leftJoinAndSelect('project.organization', 'organization')
|
.leftJoinAndSelect('project.organization', 'organization')
|
||||||
.leftJoin('project.projectMembers', 'projectMembers')
|
.leftJoin('project.projectMembers', 'projectMembers')
|
||||||
.where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', {
|
.where(
|
||||||
|
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText',
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
searchText: `%${searchText}%`
|
searchText: `%${searchText}%`
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentById (deploymentId: string, data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeploymentById(
|
||||||
|
deploymentId: string,
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
return this.updateDeployment({ id: deploymentId }, data);
|
return this.updateDeployment({ id: deploymentId }, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeployment (criteria: FindOptionsWhere<Deployment>, data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeployment(
|
||||||
|
criteria: FindOptionsWhere<Deployment>,
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const updateResult = await deploymentRepository.update(criteria, data);
|
const updateResult = await deploymentRepository.update(criteria, data);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeploymentsByProjectIds(
|
||||||
|
projectIds: string[],
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
|
|
||||||
const updateResult = await deploymentRepository
|
const updateResult = await deploymentRepository
|
||||||
@ -329,7 +478,20 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProject (userId: string, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
|
async deleteDeploymentById(deploymentId: string): Promise<boolean> {
|
||||||
|
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
|
||||||
@ -339,27 +501,35 @@ export class Database {
|
|||||||
// TODO: Set icon according to framework
|
// TODO: Set icon according to framework
|
||||||
newProject.icon = '';
|
newProject.icon = '';
|
||||||
|
|
||||||
newProject.owner = Object.assign(new User(), {
|
newProject.owner = user;
|
||||||
id: userId
|
|
||||||
});
|
|
||||||
|
|
||||||
newProject.organization = Object.assign(new Organization(), {
|
newProject.organization = Object.assign(new Organization(), {
|
||||||
id: organizationId
|
id: organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
newProject.subDomain = `${newProject.name}.${PROJECT_DOMAIN}`;
|
|
||||||
|
|
||||||
return projectRepository.save(newProject);
|
return projectRepository.save(newProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectById (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
|
async saveProject(project: Project): Promise<Project> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
const updateResult = await projectRepository.update({ id: projectId }, data);
|
|
||||||
|
return projectRepository.save(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProjectById(
|
||||||
|
projectId: string,
|
||||||
|
data: DeepPartial<Project>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
const updateResult = await projectRepository.update(
|
||||||
|
{ id: projectId },
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteProjectById (projectId: string): Promise<boolean> {
|
async deleteProjectById(projectId: string): Promise<boolean> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
const project = await projectRepository.findOneOrFail({
|
const project = await projectRepository.findOneOrFail({
|
||||||
where: {
|
where: {
|
||||||
@ -375,7 +545,7 @@ export class Database {
|
|||||||
return Boolean(deleteResult);
|
return Boolean(deleteResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteDomainById (domainId: string): Promise<boolean> {
|
async deleteDomainById(domainId: string): Promise<boolean> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
|
|
||||||
const deleteResult = await domainRepository.softDelete({ id: domainId });
|
const deleteResult = await domainRepository.softDelete({ id: domainId });
|
||||||
@ -387,28 +557,34 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDomain (data: DeepPartial<Domain>): Promise<Domain> {
|
async addDomain(data: DeepPartial<Domain>): Promise<Domain> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const newDomain = await domainRepository.save(data);
|
const newDomain = await domainRepository.save(data);
|
||||||
|
|
||||||
return newDomain;
|
return newDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomain (options: FindOneOptions<Domain>): Promise<Domain | null> {
|
async getDomain(options: FindOneOptions<Domain>): Promise<Domain | null> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const domain = await domainRepository.findOne(options);
|
const domain = await domainRepository.findOne(options);
|
||||||
|
|
||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
|
async updateDomainById(
|
||||||
|
domainId: string,
|
||||||
|
data: DeepPartial<Domain>
|
||||||
|
): Promise<boolean> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const updateResult = await domainRepository.update({ id: domainId }, data);
|
const updateResult = await domainRepository.update({ id: domainId }, data);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
async getDomainsByProjectId(
|
||||||
|
projectId: string,
|
||||||
|
filter?: FindOptionsWhere<Domain>
|
||||||
|
): Promise<Domain[]> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
|
|
||||||
const domains = await domainRepository.find({
|
const domains = await domainRepository.find({
|
||||||
@ -425,4 +601,24 @@ export class Database {
|
|||||||
|
|
||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addDeployer(data: DeepPartial<Deployer>): Promise<Deployer> {
|
||||||
|
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||||
|
const newDomain = await deployerRepository.save(data);
|
||||||
|
|
||||||
|
return newDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeployers(): Promise<Deployer[]> {
|
||||||
|
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||||
|
const deployers = await deployerRepository.find();
|
||||||
|
return deployers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeployerByLRN(deployerLrn: string): Promise<Deployer | null> {
|
||||||
|
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||||
|
const deployer = await deployerRepository.findOne({ where: { deployerLrn } });
|
||||||
|
|
||||||
|
return deployer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
26
packages/backend/src/entity/Deployer.ts
Normal file
26
packages/backend/src/entity/Deployer.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Entity, PrimaryColumn, Column, ManyToMany } from 'typeorm';
|
||||||
|
import { Project } from './Project';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Deployer {
|
||||||
|
@PrimaryColumn('varchar')
|
||||||
|
deployerLrn!: string;
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
deployerId!: string;
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
deployerApiUrl!: string;
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
baseDomain!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
minimumPayment!: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
paymentAddress!: string | null;
|
||||||
|
|
||||||
|
@ManyToMany(() => Project, (project) => project.deployers)
|
||||||
|
projects!: Project[];
|
||||||
|
}
|
@ -6,13 +6,15 @@ 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 } from '../types';
|
import { Deployer } from './Deployer';
|
||||||
|
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
|
||||||
|
|
||||||
export enum Environment {
|
export enum Environment {
|
||||||
Production = 'Production',
|
Production = 'Production',
|
||||||
@ -24,20 +26,42 @@ 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;
|
||||||
|
lrn?: string;
|
||||||
|
auction?: string;
|
||||||
|
config: string;
|
||||||
|
meta: string;
|
||||||
|
payment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationDeploymentRemovalRequest {
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
deployment: string;
|
||||||
|
auction?: string;
|
||||||
|
payment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ApplicationRecord {
|
export interface ApplicationRecord {
|
||||||
type: string;
|
type: string;
|
||||||
version:string
|
version: string;
|
||||||
name: string
|
name: string;
|
||||||
description?: string
|
description?: string;
|
||||||
homepage?: string
|
homepage?: string;
|
||||||
license?: string
|
license?: string;
|
||||||
author?: string
|
author?: string;
|
||||||
repository?: string[],
|
repository?: string[];
|
||||||
app_version?: string
|
app_version?: string;
|
||||||
repository_ref: string
|
repository_ref: string;
|
||||||
app_type: string
|
app_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@ -78,12 +102,34 @@ 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;
|
||||||
|
|
||||||
|
@ManyToOne(() => Deployer)
|
||||||
|
@JoinColumn({ name: 'deployerLrn' })
|
||||||
|
deployer!: Deployer;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
enum: Environment
|
enum: Environment
|
||||||
})
|
})
|
||||||
@ -106,4 +152,7 @@ export class Deployment {
|
|||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt!: Date | null;
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,12 @@ export class Organization {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
|
@OneToMany(
|
||||||
|
() => UserOrganization,
|
||||||
|
(userOrganization) => userOrganization.organization,
|
||||||
|
{
|
||||||
cascade: ['soft-remove']
|
cascade: ['soft-remove']
|
||||||
})
|
}
|
||||||
|
)
|
||||||
userOrganizations!: UserOrganization[];
|
userOrganizations!: UserOrganization[];
|
||||||
}
|
}
|
||||||
|
@ -7,22 +7,16 @@ import {
|
|||||||
ManyToOne,
|
ManyToOne,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
DeleteDateColumn
|
DeleteDateColumn,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { Organization } from './Organization';
|
import { Organization } from './Organization';
|
||||||
import { ProjectMember } from './ProjectMember';
|
import { ProjectMember } from './ProjectMember';
|
||||||
import { Deployment } from './Deployment';
|
import { Deployment } from './Deployment';
|
||||||
|
import { Deployer } from './Deployer';
|
||||||
export interface ApplicationDeploymentRequest {
|
|
||||||
type: string
|
|
||||||
version: string
|
|
||||||
name: string
|
|
||||||
application: string
|
|
||||||
config: string,
|
|
||||||
meta: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Project {
|
export class Project {
|
||||||
@ -52,15 +46,23 @@ 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;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
auctionId!: string | null;
|
||||||
|
|
||||||
|
// Tx hash for sending coins from snowball to deployer
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
txHash!: string | null;
|
||||||
|
|
||||||
|
@ManyToMany(() => Deployer, (deployer) => (deployer.projects))
|
||||||
|
@JoinTable()
|
||||||
|
deployers!: Deployer[]
|
||||||
|
|
||||||
|
@Column('boolean', { default: false, nullable: true })
|
||||||
|
fundsReleased!: boolean;
|
||||||
|
|
||||||
// TODO: Compute template & framework in import repository
|
// TODO: Compute template & framework in import repository
|
||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
template!: string | null;
|
template!: string | null;
|
||||||
@ -68,6 +70,10 @@ export class Project {
|
|||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
framework!: string | null;
|
framework!: string | null;
|
||||||
|
|
||||||
|
// Address of the user who created the project i.e. requested deployments
|
||||||
|
@Column('varchar')
|
||||||
|
paymentAddress!: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: 'simple-array'
|
type: 'simple-array'
|
||||||
})
|
})
|
||||||
@ -76,9 +82,6 @@ export class Project {
|
|||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
icon!: string;
|
icon!: string;
|
||||||
|
|
||||||
@Column('varchar')
|
|
||||||
subDomain!: string;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
@ -91,7 +94,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[];
|
||||||
|
@ -15,7 +15,7 @@ import { User } from './User';
|
|||||||
|
|
||||||
export enum Permission {
|
export enum Permission {
|
||||||
View = 'View',
|
View = 'View',
|
||||||
Edit = 'Edit'
|
Edit = 'Edit',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@ -12,10 +12,15 @@ 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;
|
||||||
|
|
||||||
@ -34,13 +39,23 @@ export class User {
|
|||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
@Column()
|
||||||
|
subOrgId!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
turnkeyWalletId!: string;
|
||||||
|
|
||||||
|
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||||
cascade: ['soft-remove']
|
cascade: ['soft-remove']
|
||||||
})
|
})
|
||||||
projectMembers!: ProjectMember[];
|
projectMembers!: ProjectMember[];
|
||||||
|
|
||||||
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
|
@OneToMany(
|
||||||
|
() => UserOrganization,
|
||||||
|
(UserOrganization) => UserOrganization.member,
|
||||||
|
{
|
||||||
cascade: ['soft-remove']
|
cascade: ['soft-remove']
|
||||||
})
|
}
|
||||||
|
)
|
||||||
userOrganizations!: UserOrganization[];
|
userOrganizations!: UserOrganization[];
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { Organization } from './Organization';
|
import { Organization } from './Organization';
|
||||||
|
|
||||||
enum Role {
|
export enum Role {
|
||||||
Owner = 'Owner',
|
Owner = 'Owner',
|
||||||
Maintainer = 'Maintainer',
|
Maintainer = 'Maintainer',
|
||||||
Reader = 'Reader',
|
Reader = 'Reader',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'express-async-errors';
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
@ -9,8 +10,6 @@ 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';
|
||||||
|
|
||||||
@ -18,22 +17,28 @@ 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> => {
|
||||||
// TODO: get config path using cli
|
const { server, database, gitHub, registryConfig } = await getConfig();
|
||||||
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);
|
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({ gitHubConfig: gitHub, registryConfig }, db, app, registry);
|
const service = new Service(
|
||||||
|
{ gitHubConfig: gitHub, registryConfig },
|
||||||
|
db,
|
||||||
|
app,
|
||||||
|
registry,
|
||||||
|
);
|
||||||
|
|
||||||
const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString();
|
const typeDefs = fs
|
||||||
|
.readFileSync(path.join(__dirname, 'schema.gql'))
|
||||||
|
.toString();
|
||||||
const resolvers = await createResolvers(service);
|
const resolvers = await createResolvers(service);
|
||||||
|
|
||||||
await createAndStartServer(server, typeDefs, resolvers, service);
|
await createAndStartServer(server, typeDefs, resolvers, service);
|
||||||
|
@ -1,55 +1,89 @@
|
|||||||
import debug from 'debug';
|
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { inc as semverInc } from 'semver';
|
import debug from 'debug';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { Octokit } from 'octokit';
|
||||||
|
import { inc as semverInc } from 'semver';
|
||||||
|
import { DeepPartial } from 'typeorm';
|
||||||
|
|
||||||
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
import { Account, DEFAULT_GAS_ESTIMATION_MULTIPLIER, Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
|
||||||
|
import { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate';
|
||||||
|
|
||||||
import { RegistryConfig } from './config';
|
import { RegistryConfig } from './config';
|
||||||
import { ApplicationDeploymentRequest } from './entity/Project';
|
import {
|
||||||
import { ApplicationRecord, Deployment } from './entity/Deployment';
|
ApplicationRecord,
|
||||||
import { AppDeploymentRecord, PackageJSON } from './types';
|
Deployment,
|
||||||
|
ApplicationDeploymentRequest,
|
||||||
|
ApplicationDeploymentRemovalRequest
|
||||||
|
} from './entity/Deployment';
|
||||||
|
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types';
|
||||||
|
import { getConfig, getRepoDetails, registryTransactionWithRetry, 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_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction';
|
||||||
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 WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer'
|
||||||
|
const SLEEP_DURATION = 1000;
|
||||||
|
|
||||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
// TODO: Move registry code to registry-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(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
|
||||||
|
const gasPrice = getGasPrice(registryConfig.fee.gasPrice);
|
||||||
|
|
||||||
|
this.registry = new LaconicRegistry(
|
||||||
|
registryConfig.gqlEndpoint,
|
||||||
|
registryConfig.restEndpoint,
|
||||||
|
{ chainId: registryConfig.chainId, gasPrice }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationRecord ({
|
async createApplicationRecord({
|
||||||
packageJSON,
|
octokit,
|
||||||
|
repository,
|
||||||
commitHash,
|
commitHash,
|
||||||
appType,
|
appType,
|
||||||
repoUrl
|
|
||||||
}: {
|
}: {
|
||||||
packageJSON: PackageJSON
|
octokit: Octokit
|
||||||
commitHash: string,
|
repository: string;
|
||||||
appType: string,
|
commitHash: string;
|
||||||
repoUrl: string
|
appType: string;
|
||||||
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> {
|
}): Promise<{
|
||||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
applicationRecordId: string;
|
||||||
// Use laconic-sdk to publish record
|
applicationRecordData: ApplicationRecord;
|
||||||
|
}> {
|
||||||
|
const { repo, repoUrl, packageJSON } = await getRepoDetails(octokit, repository, commitHash)
|
||||||
|
// Use registry-sdk to publish record
|
||||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||||
// Fetch previous records
|
// Fetch previous records
|
||||||
const records = await this.registry.queryRecords({
|
const records = await this.registry.queryRecords(
|
||||||
|
{
|
||||||
type: APP_RECORD_TYPE,
|
type: APP_RECORD_TYPE,
|
||||||
name: packageJSON.name
|
name: packageJSON.name
|
||||||
}, true);
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Get next version of record
|
// Get next version of record
|
||||||
const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId);
|
const bondRecords = records.filter(
|
||||||
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
|
(record: any) => record.bondId === this.registryConfig.bondId
|
||||||
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch');
|
);
|
||||||
|
const [latestBondRecord] = bondRecords.sort(
|
||||||
|
(a: any, b: any) =>
|
||||||
|
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||||
|
);
|
||||||
|
const nextVersion = semverInc(
|
||||||
|
latestBondRecord?.attributes.version ?? '0.0.0',
|
||||||
|
'patch'
|
||||||
|
);
|
||||||
|
|
||||||
assert(nextVersion, 'Application record version not valid');
|
assert(nextVersion, 'Application record version not valid');
|
||||||
|
|
||||||
@ -60,52 +94,171 @@ export class Registry {
|
|||||||
repository_ref: commitHash,
|
repository_ref: commitHash,
|
||||||
repository: [repoUrl],
|
repository: [repoUrl],
|
||||||
app_type: appType,
|
app_type: appType,
|
||||||
name: packageJSON.name,
|
name: repo,
|
||||||
...(packageJSON.description && { description: packageJSON.description }),
|
...(packageJSON.description && { description: packageJSON.description }),
|
||||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||||
...(packageJSON.license && { license: packageJSON.license }),
|
...(packageJSON.license && { license: packageJSON.license }),
|
||||||
...(packageJSON.author && { author: typeof packageJSON.author === 'object' ? JSON.stringify(packageJSON.author) : packageJSON.author }),
|
...(packageJSON.author && {
|
||||||
|
author:
|
||||||
|
typeof packageJSON.author === 'object'
|
||||||
|
? JSON.stringify(packageJSON.author)
|
||||||
|
: packageJSON.author
|
||||||
|
}),
|
||||||
...(packageJSON.version && { app_version: packageJSON.version })
|
...(packageJSON.version && { app_version: packageJSON.version })
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.registry.setRecord(
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
|
||||||
|
const result = await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.setRecord(
|
||||||
{
|
{
|
||||||
privateKey: this.registryConfig.privateKey,
|
privateKey: this.registryConfig.privateKey,
|
||||||
record: applicationRecord,
|
record: applicationRecord,
|
||||||
bondId: this.registryConfig.bondId
|
bondId: this.registryConfig.bondId
|
||||||
},
|
},
|
||||||
'',
|
this.registryConfig.privateKey,
|
||||||
this.registryConfig.fee
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
log(`Published application record ${result.id}`);
|
||||||
log('Application record data:', applicationRecord);
|
log('Application record data:', applicationRecord);
|
||||||
|
|
||||||
// TODO: Discuss computation of CRN
|
// TODO: Discuss computation of LRN
|
||||||
const crn = this.getCrn(packageJSON.name);
|
const lrn = this.getLrn(repo);
|
||||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
log(`Setting name: ${lrn} for record ID: ${result.id}`);
|
||||||
|
|
||||||
await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee);
|
await sleep(SLEEP_DURATION);
|
||||||
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
await registryTransactionWithRetry(() =>
|
||||||
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
this.registry.setName(
|
||||||
|
{
|
||||||
|
cid: result.id,
|
||||||
|
lrn
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord };
|
await sleep(SLEEP_DURATION);
|
||||||
|
await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.setName(
|
||||||
|
{
|
||||||
|
cid: result.id,
|
||||||
|
lrn: `${lrn}@${applicationRecord.app_version}`
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await sleep(SLEEP_DURATION);
|
||||||
|
await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.setName(
|
||||||
|
{
|
||||||
|
cid: result.id,
|
||||||
|
lrn: `${lrn}@${applicationRecord.repository_ref}`
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
applicationRecordId: result.id,
|
||||||
|
applicationRecordData: applicationRecord
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationDeploymentRequest (data: {
|
async createApplicationDeploymentAuction(
|
||||||
appName: string,
|
appName: string,
|
||||||
commitHash: string,
|
octokit: Octokit,
|
||||||
repository: string,
|
auctionParams: AuctionParams,
|
||||||
environmentVariables: { [key: string]: string }
|
data: DeepPartial<Deployment>,
|
||||||
}): Promise<{
|
): Promise<{
|
||||||
applicationDeploymentRequestId: string,
|
applicationDeploymentAuctionId: string;
|
||||||
applicationDeploymentRequestData: ApplicationDeploymentRequest
|
|
||||||
}> {
|
}> {
|
||||||
const crn = this.getCrn(data.appName);
|
assert(data.project?.repository, 'Project repository not found');
|
||||||
const records = await this.registry.resolveNames([crn]);
|
|
||||||
|
await this.createApplicationRecord({
|
||||||
|
octokit,
|
||||||
|
repository: data.project.repository,
|
||||||
|
appType: data.project!.template!,
|
||||||
|
commitHash: data.commitHash!,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lrn = this.getLrn(appName);
|
||||||
|
const config = await getConfig();
|
||||||
|
const auctionConfig = config.auction;
|
||||||
|
|
||||||
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
const auctionResult = await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.createProviderAuction(
|
||||||
|
{
|
||||||
|
commitFee: auctionConfig.commitFee,
|
||||||
|
commitsDuration: auctionConfig.commitsDuration,
|
||||||
|
revealFee: auctionConfig.revealFee,
|
||||||
|
revealsDuration: auctionConfig.revealsDuration,
|
||||||
|
denom: auctionConfig.denom,
|
||||||
|
maxPrice: auctionParams.maxPrice,
|
||||||
|
numProviders: auctionParams.numProviders,
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!auctionResult.auction) {
|
||||||
|
throw new Error('Error creating auction');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create record of type applicationDeploymentAuction and publish
|
||||||
|
const applicationDeploymentAuction = {
|
||||||
|
application: lrn,
|
||||||
|
auction: auctionResult.auction.id,
|
||||||
|
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.setRecord(
|
||||||
|
{
|
||||||
|
privateKey: this.registryConfig.privateKey,
|
||||||
|
record: applicationDeploymentAuction,
|
||||||
|
bondId: this.registryConfig.bondId
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
log(`Application deployment auction created: ${auctionResult.auction.id}`);
|
||||||
|
log(`Application deployment auction record published: ${result.id}`);
|
||||||
|
log('Application deployment auction data:', applicationDeploymentAuction);
|
||||||
|
|
||||||
|
return {
|
||||||
|
applicationDeploymentAuctionId: auctionResult.auction.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createApplicationDeploymentRequest(data: {
|
||||||
|
deployment: Deployment,
|
||||||
|
appName: string,
|
||||||
|
repository: string,
|
||||||
|
auctionId?: string | null,
|
||||||
|
lrn: string,
|
||||||
|
environmentVariables: { [key: string]: string },
|
||||||
|
dns: string,
|
||||||
|
payment?: string | null
|
||||||
|
}): Promise<{
|
||||||
|
applicationDeploymentRequestId: string;
|
||||||
|
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||||
|
}> {
|
||||||
|
const lrn = this.getLrn(data.appName);
|
||||||
|
const records = await this.registry.resolveNames([lrn]);
|
||||||
const applicationRecord = records[0];
|
const applicationRecord = records[0];
|
||||||
|
|
||||||
if (!applicationRecord) {
|
if (!applicationRecord) {
|
||||||
throw new Error(`No record found for ${crn}`);
|
throw new Error(`No record found for ${lrn}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create record of type ApplicationDeploymentRequest and publish
|
// Create record of type ApplicationDeploymentRequest and publish
|
||||||
@ -113,60 +266,270 @@ export class Registry {
|
|||||||
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
||||||
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: `${lrn}@${applicationRecord.attributes.app_version}`,
|
||||||
|
dns: data.dns,
|
||||||
// TODO: Not set in test-progressive-web-app CI
|
|
||||||
// dns: '$CERC_REGISTRY_DEPLOYMENT_SHORT_HOSTNAME',
|
|
||||||
// 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
|
||||||
config: JSON.stringify({
|
config: JSON.stringify({
|
||||||
env: data.environmentVariables
|
env: data.environmentVariables
|
||||||
}),
|
}),
|
||||||
meta: JSON.stringify({
|
meta: JSON.stringify({
|
||||||
note: `Added by Snowball @ ${DateTime.utc().toFormat('EEE LLL dd HH:mm:ss \'UTC\' yyyy')}`,
|
note: `Added by Snowball @ ${DateTime.utc().toFormat(
|
||||||
|
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
|
||||||
|
)}`,
|
||||||
repository: data.repository,
|
repository: data.repository,
|
||||||
repository_ref: data.commitHash
|
repository_ref: data.deployment.commitHash
|
||||||
})
|
}),
|
||||||
|
deployer: data.lrn,
|
||||||
|
...(data.auctionId && { auction: data.auctionId }),
|
||||||
|
...(data.payment && { payment: data.payment }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.registry.setRecord(
|
await sleep(SLEEP_DURATION);
|
||||||
|
|
||||||
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
|
||||||
|
const result = await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.setRecord(
|
||||||
{
|
{
|
||||||
privateKey: this.registryConfig.privateKey,
|
privateKey: this.registryConfig.privateKey,
|
||||||
record: applicationDeploymentRequest,
|
record: applicationDeploymentRequest,
|
||||||
bondId: this.registryConfig.bondId
|
bondId: this.registryConfig.bondId
|
||||||
},
|
},
|
||||||
'',
|
this.registryConfig.privateKey,
|
||||||
this.registryConfig.fee
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
log(`Application deployment request record published: ${result.data.id}`);
|
|
||||||
|
log(`Application deployment request record published: ${result.id}`);
|
||||||
log('Application deployment request data:', applicationDeploymentRequest);
|
log('Application deployment request data:', applicationDeploymentRequest);
|
||||||
|
|
||||||
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest };
|
return {
|
||||||
|
applicationDeploymentRequestId: result.id,
|
||||||
|
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuctionWinningDeployerRecords(
|
||||||
|
auctionId: string
|
||||||
|
): Promise<DeployerRecord[]> {
|
||||||
|
const records = await this.registry.getAuctionsByIds([auctionId]);
|
||||||
|
const auctionResult = records[0];
|
||||||
|
|
||||||
|
let deployerRecords = [];
|
||||||
|
const { winnerAddresses } = auctionResult;
|
||||||
|
|
||||||
|
for (const auctionWinner of winnerAddresses) {
|
||||||
|
const records = await this.getDeployerRecordsByFilter({
|
||||||
|
paymentAddress: auctionWinner,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newRecords = records.filter(record => {
|
||||||
|
return record.names !== null && record.names.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const record of newRecords) {
|
||||||
|
if (record.id) {
|
||||||
|
deployerRecords.push(record);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deployerRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseDeployerFunds(
|
||||||
|
auctionId: string
|
||||||
|
): Promise<any> {
|
||||||
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
const auction = await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.releaseFunds(
|
||||||
|
{
|
||||||
|
auctionId
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return auction;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch ApplicationDeploymentRecords for deployments
|
* Fetch ApplicationDeploymentRecords for deployments
|
||||||
*/
|
*/
|
||||||
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> {
|
async getDeploymentRecords(
|
||||||
|
deployments: Deployment[]
|
||||||
|
): Promise<AppDeploymentRecord[]> {
|
||||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||||
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
||||||
const records = await this.registry.queryRecords({
|
const records = await this.registry.queryRecords(
|
||||||
|
{
|
||||||
type: APP_DEPLOYMENT_RECORD_TYPE
|
type: APP_DEPLOYMENT_RECORD_TYPE
|
||||||
}, true);
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Filter records with ApplicationRecord ids
|
// Filter records with ApplicationDeploymentRequestId ID and Deployment specific URL
|
||||||
return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application));
|
return records.filter((record: AppDeploymentRecord) =>
|
||||||
|
deployments.some(
|
||||||
|
(deployment) =>
|
||||||
|
deployment.applicationDeploymentRequestId === record.attributes.request &&
|
||||||
|
record.attributes.url.includes(deployment.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCrn (packageJsonName: string): string {
|
/**
|
||||||
const [arg1, arg2] = packageJsonName.split('/');
|
* Fetch WebappDeployer Records by filter
|
||||||
|
*/
|
||||||
if (arg2) {
|
async getDeployerRecordsByFilter(filter: { [key: string]: any }): Promise<DeployerRecord[]> {
|
||||||
const authority = arg1.replace('@', '');
|
return this.registry.queryRecords(
|
||||||
return `crn://${authority}/applications/${arg2}`;
|
{
|
||||||
|
type: WEBAPP_DEPLOYER_RECORD_TYPE,
|
||||||
|
...filter
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `crn://${arg1}/applications/${arg1}`;
|
/**
|
||||||
|
* Fetch ApplicationDeploymentRecords by filter
|
||||||
|
*/
|
||||||
|
async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
||||||
|
return this.registry.queryRecords(
|
||||||
|
{
|
||||||
|
type: APP_DEPLOYMENT_RECORD_TYPE,
|
||||||
|
...filter
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||||
|
*/
|
||||||
|
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 records.filter((record: AppDeploymentRemovalRecord) =>
|
||||||
|
deployments.some(
|
||||||
|
(deployment) =>
|
||||||
|
deployment.applicationDeploymentRemovalRequestId === record.attributes.request &&
|
||||||
|
deployment.applicationDeploymentRecordId === record.attributes.deployment
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createApplicationDeploymentRemovalRequest(data: {
|
||||||
|
deploymentId: string;
|
||||||
|
deployerLrn: string;
|
||||||
|
auctionId?: string | null;
|
||||||
|
payment?: string | null;
|
||||||
|
}): Promise<{
|
||||||
|
applicationDeploymentRemovalRequestId: string;
|
||||||
|
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
||||||
|
}> {
|
||||||
|
const applicationDeploymentRemovalRequest = {
|
||||||
|
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
||||||
|
version: '1.0.0',
|
||||||
|
deployment: data.deploymentId,
|
||||||
|
deployer: data.deployerLrn,
|
||||||
|
...(data.auctionId && { auction: data.auctionId }),
|
||||||
|
...(data.payment && { payment: data.payment }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
|
||||||
|
const result = await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.setRecord(
|
||||||
|
{
|
||||||
|
privateKey: this.registryConfig.privateKey,
|
||||||
|
record: applicationDeploymentRemovalRequest,
|
||||||
|
bondId: this.registryConfig.bondId
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
log(`Application deployment removal request record published: ${result.id}`);
|
||||||
|
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
|
||||||
|
|
||||||
|
return {
|
||||||
|
applicationDeploymentRemovalRequestId: result.id,
|
||||||
|
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> {
|
||||||
|
if (auctionIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const auctions = await this.registry.getAuctionsByIds(auctionIds);
|
||||||
|
|
||||||
|
const completedAuctions = auctions
|
||||||
|
.filter((auction: { id: string, status: string }) => auction.status === 'completed')
|
||||||
|
.map((auction: { id: string, status: string }) => auction.id);
|
||||||
|
|
||||||
|
return completedAuctions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRecordsByName(name: string): Promise<any> {
|
||||||
|
return this.registry.resolveNames([name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuctionData(auctionId: string): Promise<any> {
|
||||||
|
return this.registry.getAuctionsByIds([auctionId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTokensToAccount(receiverAddress: string, amount: string): Promise<DeliverTxResponse> {
|
||||||
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
const account = await this.getAccount();
|
||||||
|
const laconicClient = await this.registry.getLaconicClient(account);
|
||||||
|
const txResponse: DeliverTxResponse =
|
||||||
|
await registryTransactionWithRetry(() =>
|
||||||
|
laconicClient.sendTokens(account.address, receiverAddress,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
denom: 'alnt',
|
||||||
|
amount
|
||||||
|
}
|
||||||
|
],
|
||||||
|
fee || DEFAULT_GAS_ESTIMATION_MULTIPLIER)
|
||||||
|
);
|
||||||
|
|
||||||
|
return txResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccount(): Promise<Account> {
|
||||||
|
const account = new Account(Buffer.from(this.registryConfig.privateKey, 'hex'));
|
||||||
|
await account.init();
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTxResponse(txHash: string): Promise<IndexedTx | null> {
|
||||||
|
const account = await this.getAccount();
|
||||||
|
const laconicClient = await this.registry.getLaconicClient(account);
|
||||||
|
const txResponse: IndexedTx | null = await laconicClient.getTx(txHash);
|
||||||
|
|
||||||
|
return txResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLrn(appName: string): string {
|
||||||
|
assert(this.registryConfig.authority, "Authority doesn't exist");
|
||||||
|
return `lrn://${this.registryConfig.authority}/applications/${appName}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { Permission } from './entity/ProjectMember';
|
|||||||
import { Domain } from './entity/Domain';
|
import { Domain } from './entity/Domain';
|
||||||
import { Project } from './entity/Project';
|
import { Project } from './entity/Project';
|
||||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||||
|
import { AddProjectFromTemplateInput, AuctionParams, EnvironmentVariables } from './types';
|
||||||
|
|
||||||
const log = debug('snowball:resolver');
|
const log = debug('snowball:resolver');
|
||||||
|
|
||||||
@ -14,26 +15,36 @@ 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 service.getUser(context.userId);
|
return context.user;
|
||||||
},
|
},
|
||||||
|
|
||||||
organizations: async (_:any, __: any, context: any) => {
|
organizations: async (_: any, __: any, context: any) => {
|
||||||
return service.getOrganizationsByUserId(context.userId);
|
return service.getOrganizationsByUserId(context.user);
|
||||||
},
|
},
|
||||||
|
|
||||||
project: async (_: any, { projectId }: { projectId: string }) => {
|
project: async (_: any, { projectId }: { projectId: string }, context: any) => {
|
||||||
return service.getProjectById(projectId);
|
return service.getProjectById(context.user, projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
|
projectsInOrganization: async (
|
||||||
return service.getProjectsInOrganization(context.userId, organizationSlug);
|
_: any,
|
||||||
|
{ organizationSlug }: { organizationSlug: string },
|
||||||
|
context: any,
|
||||||
|
) => {
|
||||||
|
return service.getProjectsInOrganization(
|
||||||
|
context.user,
|
||||||
|
organizationSlug,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
return service.getDeploymentsByProjectId(projectId);
|
return service.getDeploymentsByProjectId(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
|
environmentVariables: async (
|
||||||
|
_: any,
|
||||||
|
{ projectId }: { projectId: string },
|
||||||
|
) => {
|
||||||
return service.getEnvironmentVariablesByProjectId(projectId);
|
return service.getEnvironmentVariablesByProjectId(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -41,32 +52,81 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
return service.getProjectMembersByProjectId(projectId);
|
return service.getProjectMembersByProjectId(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
|
searchProjects: async (
|
||||||
return service.searchProjects(context.userId, searchText);
|
_: any,
|
||||||
|
{ searchText }: { searchText: string },
|
||||||
|
context: any,
|
||||||
|
) => {
|
||||||
|
return service.searchProjects(context.user, searchText);
|
||||||
},
|
},
|
||||||
|
|
||||||
domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere<Domain> }) => {
|
domains: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
filter,
|
||||||
|
}: { projectId: string; filter?: FindOptionsWhere<Domain> },
|
||||||
|
) => {
|
||||||
return service.getDomainsByProjectId(projectId, filter);
|
return service.getDomainsByProjectId(projectId, filter);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
getAuctionData: async (
|
||||||
|
_: any,
|
||||||
|
{ auctionId }: { auctionId: string },
|
||||||
|
) => {
|
||||||
|
return service.getAuctionData(auctionId);
|
||||||
|
},
|
||||||
|
|
||||||
|
deployers: async (_: any, __: any, context: any) => {
|
||||||
|
return service.getDeployers();
|
||||||
|
},
|
||||||
|
|
||||||
|
address: async (_: any, __: any, context: any) => {
|
||||||
|
return service.getAddress();
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyTx: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
txHash,
|
||||||
|
amount,
|
||||||
|
senderAddress,
|
||||||
|
}: { txHash: string; amount: string; senderAddress: string },
|
||||||
|
) => {
|
||||||
|
return service.verifyTx(txHash, amount, senderAddress);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Return error in GQL response
|
// TODO: Return error in GQL response
|
||||||
Mutation: {
|
Mutation: {
|
||||||
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
|
removeProjectMember: async (
|
||||||
|
_: any,
|
||||||
|
{ projectMemberId }: { projectMemberId: string },
|
||||||
|
context: any,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.removeProjectMember(context.userId, projectMemberId);
|
return await service.removeProjectMember(
|
||||||
|
context.user,
|
||||||
|
projectMemberId,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProjectMember: async (_: any, { projectMemberId, data }: {
|
updateProjectMember: async (
|
||||||
projectMemberId: string,
|
_: any,
|
||||||
|
{
|
||||||
|
projectMemberId,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
projectMemberId: string;
|
||||||
data: {
|
data: {
|
||||||
permissions: Permission[]
|
permissions: Permission[];
|
||||||
}
|
};
|
||||||
}) => {
|
},
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateProjectMember(projectMemberId, data);
|
return await service.updateProjectMember(projectMemberId, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -75,13 +135,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addProjectMember: async (_: any, { projectId, data }: {
|
addProjectMember: async (
|
||||||
projectId: string,
|
_: any,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
data: {
|
data: {
|
||||||
email: string,
|
email: string;
|
||||||
permissions: Permission[]
|
permissions: Permission[];
|
||||||
}
|
};
|
||||||
}) => {
|
},
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.addProjectMember(projectId, data));
|
return Boolean(await service.addProjectMember(projectId, data));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -90,25 +156,51 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addEnvironmentVariables: async (_: any, { projectId, data }: { projectId: string, data: { environments: string[], key: string, value: string}[] }) => {
|
addEnvironmentVariables: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
data: { environments: string[]; key: string; value: string }[];
|
||||||
|
},
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.addEnvironmentVariables(projectId, data));
|
return Boolean(
|
||||||
|
await service.addEnvironmentVariables(projectId, data),
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => {
|
updateEnvironmentVariable: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
environmentVariableId,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
environmentVariableId: string;
|
||||||
|
data: DeepPartial<EnvironmentVariable>;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateEnvironmentVariable(environmentVariableId, data);
|
return await service.updateEnvironmentVariable(
|
||||||
|
environmentVariableId,
|
||||||
|
data,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
|
removeEnvironmentVariable: async (
|
||||||
|
_: any,
|
||||||
|
{ environmentVariableId }: { environmentVariableId: string },
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.removeEnvironmentVariable(environmentVariableId);
|
return await service.removeEnvironmentVariable(environmentVariableId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -117,25 +209,89 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
|
updateDeploymentToProd: async (
|
||||||
|
_: any,
|
||||||
|
{ deploymentId }: { deploymentId: string },
|
||||||
|
context: any,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.updateDeploymentToProd(context.userId, deploymentId));
|
return Boolean(
|
||||||
|
await service.updateDeploymentToProd(context.user, deploymentId),
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addProject: async (_: any, { organizationSlug, data }: { organizationSlug: string, data: DeepPartial<Project> }, context: any) => {
|
addProjectFromTemplate: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
organizationSlug,
|
||||||
|
data,
|
||||||
|
lrn,
|
||||||
|
auctionParams,
|
||||||
|
environmentVariables
|
||||||
|
}: {
|
||||||
|
organizationSlug: string;
|
||||||
|
data: AddProjectFromTemplateInput;
|
||||||
|
lrn: string;
|
||||||
|
auctionParams: AuctionParams;
|
||||||
|
environmentVariables: EnvironmentVariables[];
|
||||||
|
},
|
||||||
|
context: any,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.addProject(context.userId, organizationSlug, data);
|
return await service.addProjectFromTemplate(
|
||||||
|
context.user,
|
||||||
|
organizationSlug,
|
||||||
|
data,
|
||||||
|
lrn,
|
||||||
|
auctionParams,
|
||||||
|
environmentVariables
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProject: async (_: any, { projectId, data }: { projectId: string, data: DeepPartial<Project> }) => {
|
addProject: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
organizationSlug,
|
||||||
|
data,
|
||||||
|
lrn,
|
||||||
|
auctionParams,
|
||||||
|
environmentVariables
|
||||||
|
}: {
|
||||||
|
organizationSlug: string;
|
||||||
|
data: DeepPartial<Project>;
|
||||||
|
lrn: string;
|
||||||
|
auctionParams: AuctionParams,
|
||||||
|
environmentVariables: EnvironmentVariables[];
|
||||||
|
},
|
||||||
|
context: any,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await service.addProject(
|
||||||
|
context.user,
|
||||||
|
organizationSlug,
|
||||||
|
data,
|
||||||
|
lrn,
|
||||||
|
auctionParams,
|
||||||
|
environmentVariables
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
log(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateProject: async (
|
||||||
|
_: any,
|
||||||
|
{ projectId, data }: { projectId: string; data: DeepPartial<Project> },
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateProject(projectId, data);
|
return await service.updateProject(projectId, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -144,9 +300,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
|
redeployToProd: async (
|
||||||
|
_: any,
|
||||||
|
{ deploymentId }: { deploymentId: string },
|
||||||
|
context: any,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.redeployToProd(context.userId, deploymentId));
|
return Boolean(
|
||||||
|
await service.redeployToProd(context.user, deploymentId),
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@ -157,7 +319,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
try {
|
try {
|
||||||
return await service.deleteProject(projectId);
|
return await service.deleteProject(projectId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err); return false;
|
log(err);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -170,7 +333,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => {
|
rollbackDeployment: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
deploymentId,
|
||||||
|
}: { deploymentId: string; projectId: string },
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.rollbackDeployment(projectId, deploymentId);
|
return await service.rollbackDeployment(projectId, deploymentId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -179,7 +348,22 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addDomain: async (_: any, { projectId, data }: { projectId: string, data: { name: string } }) => {
|
deleteDeployment: async (
|
||||||
|
_: 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) {
|
||||||
@ -188,7 +372,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDomain: async (_: any, { domainId, data }: { domainId: string, data: DeepPartial<Domain>}) => {
|
updateDomain: async (
|
||||||
|
_: any,
|
||||||
|
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> },
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateDomain(domainId, data);
|
return await service.updateDomain(domainId, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -197,9 +384,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => {
|
authenticateGitHub: async (
|
||||||
|
_: any,
|
||||||
|
{ code }: { code: string },
|
||||||
|
context: any,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.authenticateGitHub(code, context.userId);
|
return await service.authenticateGitHub(code, context.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@ -208,12 +399,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
|
|
||||||
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||||
try {
|
try {
|
||||||
return service.unauthenticateGitHub(context.userId, { gitHubToken: null });
|
return service.unauthenticateGitHub(context.user, {
|
||||||
|
gitHubToken: null,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
106
packages/backend/src/routes/auth.ts
Normal file
106
packages/backend/src/routes/auth.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
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) => {
|
||||||
|
console.log('Register', req.body);
|
||||||
|
const { email, challenge, attestation } = req.body;
|
||||||
|
const user = await createUser(req.app.get('service'), {
|
||||||
|
challenge,
|
||||||
|
attestation,
|
||||||
|
userEmail: email,
|
||||||
|
userName: email.split('@')[0],
|
||||||
|
});
|
||||||
|
req.session.address = user.id;
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/authenticate', async (req, res) => {
|
||||||
|
console.log('Authenticate', req.body);
|
||||||
|
const { signedWhoamiRequest } = req.body;
|
||||||
|
const user = await authenticateUser(
|
||||||
|
req.app.get('service'),
|
||||||
|
signedWhoamiRequest,
|
||||||
|
);
|
||||||
|
if (user) {
|
||||||
|
req.session.address = user.id;
|
||||||
|
res.sendStatus(200);
|
||||||
|
} else {
|
||||||
|
res.sendStatus(401);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// SIWE Auth
|
||||||
|
//
|
||||||
|
router.post('/validate', async (req, res) => {
|
||||||
|
const { message, signature } = 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 (!user) {
|
||||||
|
const newUser = await service.createUser({
|
||||||
|
ethAddress: data.address,
|
||||||
|
email: `${data.address}@example.com`,
|
||||||
|
subOrgId: '',
|
||||||
|
turnkeyWalletId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// SIWESession from the web3modal library requires both address and chain ID
|
||||||
|
req.session.address = newUser.id;
|
||||||
|
req.session.chainId = data.chainId;
|
||||||
|
} else {
|
||||||
|
req.session.address = user.id;
|
||||||
|
req.session.chainId = data.chainId;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ success });
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// General
|
||||||
|
//
|
||||||
|
router.get('/session', (req, res) => {
|
||||||
|
if (req.session.address && req.session.chainId) {
|
||||||
|
res.send({
|
||||||
|
address: req.session.address,
|
||||||
|
chainId: req.session.chainId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(401).send({ error: 'Unauthorized: No active session' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/logout', (req, res) => {
|
||||||
|
req.session.destroy((err) => {
|
||||||
|
if (err) {
|
||||||
|
return res.send({ success: false });
|
||||||
|
}
|
||||||
|
res.send({ success: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
9
packages/backend/src/routes/staging.ts
Normal file
9
packages/backend/src/routes/staging.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/version', async (req, res) => {
|
||||||
|
return res.send({ version: '0.0.9' });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
@ -19,6 +19,14 @@ enum DeploymentStatus {
|
|||||||
Building
|
Building
|
||||||
Ready
|
Ready
|
||||||
Error
|
Error
|
||||||
|
Deleting
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuctionStatus {
|
||||||
|
completed
|
||||||
|
reveal
|
||||||
|
commit
|
||||||
|
expired
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DomainStatus {
|
enum DomainStatus {
|
||||||
@ -64,8 +72,13 @@ type Project {
|
|||||||
repository: String!
|
repository: String!
|
||||||
prodBranch: String!
|
prodBranch: String!
|
||||||
description: String
|
description: String
|
||||||
|
deployers: [Deployer!]
|
||||||
|
auctionId: String
|
||||||
|
fundsReleased: Boolean
|
||||||
template: String
|
template: String
|
||||||
framework: String
|
framework: String
|
||||||
|
paymentAddress: String!
|
||||||
|
txHash: String!
|
||||||
webhooks: [String!]
|
webhooks: [String!]
|
||||||
members: [ProjectMember!]
|
members: [ProjectMember!]
|
||||||
environmentVariables: [EnvironmentVariable!]
|
environmentVariables: [EnvironmentVariable!]
|
||||||
@ -73,7 +86,7 @@ type Project {
|
|||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
organization: Organization!
|
organization: Organization!
|
||||||
icon: String
|
icon: String
|
||||||
subDomain: String
|
baseDomains: [String!]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectMember {
|
type ProjectMember {
|
||||||
@ -93,7 +106,10 @@ type Deployment {
|
|||||||
commitMessage: String!
|
commitMessage: String!
|
||||||
url: String
|
url: String
|
||||||
environment: Environment!
|
environment: Environment!
|
||||||
|
deployer: Deployer
|
||||||
|
applicationDeploymentRequestId: String
|
||||||
isCurrent: Boolean!
|
isCurrent: Boolean!
|
||||||
|
baseDomain: String
|
||||||
status: DeploymentStatus!
|
status: DeploymentStatus!
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
@ -119,6 +135,17 @@ type EnvironmentVariable {
|
|||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Deployer {
|
||||||
|
deployerLrn: String!
|
||||||
|
deployerId: String!
|
||||||
|
deployerApiUrl: String!
|
||||||
|
minimumPayment: String
|
||||||
|
paymentAddress: String
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
baseDomain: String
|
||||||
|
}
|
||||||
|
|
||||||
type AuthResult {
|
type AuthResult {
|
||||||
token: String!
|
token: String!
|
||||||
}
|
}
|
||||||
@ -129,11 +156,23 @@ input AddEnvironmentVariableInput {
|
|||||||
value: String!
|
value: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input AddProjectFromTemplateInput {
|
||||||
|
templateOwner: String!
|
||||||
|
templateRepo: String!
|
||||||
|
owner: String!
|
||||||
|
name: String!
|
||||||
|
isPrivate: Boolean!
|
||||||
|
paymentAddress: String!
|
||||||
|
txHash: String!
|
||||||
|
}
|
||||||
|
|
||||||
input AddProjectInput {
|
input AddProjectInput {
|
||||||
name: String!
|
name: String!
|
||||||
repository: String!
|
repository: String!
|
||||||
prodBranch: String!
|
prodBranch: String!
|
||||||
template: String
|
template: String
|
||||||
|
paymentAddress: String!
|
||||||
|
txHash: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateProjectInput {
|
input UpdateProjectInput {
|
||||||
@ -173,6 +212,48 @@ input FilterDomainsInput {
|
|||||||
status: DomainStatus
|
status: DomainStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Fee {
|
||||||
|
type: String!
|
||||||
|
quantity: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bid {
|
||||||
|
auctionId: String!
|
||||||
|
bidderAddress: String!
|
||||||
|
status: String!
|
||||||
|
commitHash: String!
|
||||||
|
commitTime: String
|
||||||
|
commitFee: Fee
|
||||||
|
revealTime: String
|
||||||
|
revealFee: Fee
|
||||||
|
bidAmount: Fee
|
||||||
|
}
|
||||||
|
|
||||||
|
type Auction {
|
||||||
|
id: String!
|
||||||
|
kind: String!
|
||||||
|
status: String!
|
||||||
|
ownerAddress: String!
|
||||||
|
createTime: String!
|
||||||
|
commitsEndTime: String!
|
||||||
|
revealsEndTime: String!
|
||||||
|
commitFee: Fee!
|
||||||
|
revealFee: Fee!
|
||||||
|
minimumBid: Fee
|
||||||
|
winnerAddresses: [String!]!
|
||||||
|
winnerBids: [Fee!]
|
||||||
|
winnerPrice: Fee
|
||||||
|
maxPrice: Fee
|
||||||
|
numProviders: Int!
|
||||||
|
fundsReleased: Boolean!
|
||||||
|
bids: [Bid!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
input AuctionParams {
|
||||||
|
maxPrice: String,
|
||||||
|
numProviders: Int,
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
user: User!
|
user: User!
|
||||||
organizations: [Organization!]
|
organizations: [Organization!]
|
||||||
@ -183,23 +264,50 @@ type Query {
|
|||||||
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
||||||
projectMembers(projectId: String!): [ProjectMember!]
|
projectMembers(projectId: String!): [ProjectMember!]
|
||||||
searchProjects(searchText: String!): [Project!]
|
searchProjects(searchText: String!): [Project!]
|
||||||
|
getAuctionData(auctionId: String!): Auction!
|
||||||
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
||||||
|
deployers: [Deployer]
|
||||||
|
address: String!
|
||||||
|
verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
||||||
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
|
updateProjectMember(
|
||||||
|
projectMemberId: String!
|
||||||
|
data: UpdateProjectMemberInput
|
||||||
|
): Boolean!
|
||||||
removeProjectMember(projectMemberId: String!): Boolean!
|
removeProjectMember(projectMemberId: String!): Boolean!
|
||||||
addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean!
|
addEnvironmentVariables(
|
||||||
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean!
|
projectId: String!
|
||||||
|
data: [AddEnvironmentVariableInput!]
|
||||||
|
): Boolean!
|
||||||
|
updateEnvironmentVariable(
|
||||||
|
environmentVariableId: String!
|
||||||
|
data: UpdateEnvironmentVariableInput!
|
||||||
|
): Boolean!
|
||||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||||
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
addProjectFromTemplate(
|
||||||
|
organizationSlug: String!
|
||||||
|
data: AddProjectFromTemplateInput
|
||||||
|
lrn: String
|
||||||
|
auctionParams: AuctionParams
|
||||||
|
environmentVariables: [AddEnvironmentVariableInput!]
|
||||||
|
): Project!
|
||||||
|
addProject(
|
||||||
|
organizationSlug: String!
|
||||||
|
data: AddProjectInput!
|
||||||
|
lrn: String
|
||||||
|
auctionParams: AuctionParams
|
||||||
|
environmentVariables: [AddEnvironmentVariableInput!]
|
||||||
|
): Project!
|
||||||
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
||||||
redeployToProd(deploymentId: String!): Boolean!
|
redeployToProd(deploymentId: String!): Boolean!
|
||||||
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!
|
||||||
|
@ -1,29 +1,45 @@
|
|||||||
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 session from 'express-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, USER_ID } from './constants';
|
import { DEFAULT_GQL_PATH } 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');
|
||||||
|
|
||||||
|
// Set cookie expiration to 1 month in milliseconds
|
||||||
|
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
declare module 'express-session' {
|
||||||
|
interface SessionData {
|
||||||
|
address: string;
|
||||||
|
chainId: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
@ -33,33 +49,81 @@ 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: () => {
|
context: async ({ req }) => {
|
||||||
// TODO: Use userId derived from auth token
|
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
||||||
return { userId: USER_ID };
|
|
||||||
|
const { address } = req.session;
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
throw new AuthenticationError('Unauthorized: No active session');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await service.getUser(address);
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionOptions: session.SessionOptions = {
|
||||||
|
secret: secret,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true,
|
||||||
|
cookie: {
|
||||||
|
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||||
|
maxAge: COOKIE_MAX_AGE,
|
||||||
|
domain: domain || undefined,
|
||||||
|
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (trustProxy) {
|
||||||
|
// trust first proxy
|
||||||
|
app.set('trust proxy', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
session(sessionOptions)
|
||||||
|
);
|
||||||
|
|
||||||
server.applyMiddleware({
|
server.applyMiddleware({
|
||||||
app,
|
app,
|
||||||
path: gqlPath
|
path: gqlPath,
|
||||||
|
cors: {
|
||||||
|
origin: [appOriginUrl],
|
||||||
|
credentials: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app.set('service', service);
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.set('service', service);
|
||||||
|
app.use('/auth', authRouter);
|
||||||
app.use('/api/github', githubRouter);
|
app.use('/api/github', githubRouter);
|
||||||
|
app.use('/staging', stagingRouter);
|
||||||
|
|
||||||
|
app.use((err: any, req: any, res: any, next: any) => {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
});
|
||||||
|
|
||||||
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
packages/backend/src/turnkey-backend.ts
Normal file
130
packages/backend/src/turnkey-backend.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
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;
|
||||||
|
}
|
@ -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,10 +24,13 @@ export interface GitPushEventPayload {
|
|||||||
id: string;
|
id: string;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
deleted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppDeploymentRecordAttributes {
|
export interface AppDeploymentRecordAttributes {
|
||||||
application: string;
|
application: string;
|
||||||
|
auction: string;
|
||||||
|
deployer: string;
|
||||||
dns: string;
|
dns: string;
|
||||||
meta: string;
|
meta: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -37,6 +40,13 @@ 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;
|
||||||
@ -47,5 +57,48 @@ interface RegistryRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AppDeploymentRecord extends RegistryRecord {
|
export interface AppDeploymentRecord extends RegistryRecord {
|
||||||
attributes: AppDeploymentRecordAttributes
|
attributes: AppDeploymentRecordAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppDeploymentRemovalRecord extends RegistryRecord {
|
||||||
|
attributes: AppDeploymentRemovalRecordAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddProjectFromTemplateInput {
|
||||||
|
templateOwner: string;
|
||||||
|
templateRepo: string;
|
||||||
|
owner: string;
|
||||||
|
name: string;
|
||||||
|
isPrivate: boolean;
|
||||||
|
paymentAddress: string;
|
||||||
|
txHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuctionParams {
|
||||||
|
maxPrice: string,
|
||||||
|
numProviders: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnvironmentVariables {
|
||||||
|
environments: string[],
|
||||||
|
key: string,
|
||||||
|
value: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeployerRecord {
|
||||||
|
id: string;
|
||||||
|
names: string[];
|
||||||
|
owners: string[];
|
||||||
|
bondId: string;
|
||||||
|
createTime: string;
|
||||||
|
expiryTime: string;
|
||||||
|
attributes: {
|
||||||
|
apiUrl: string;
|
||||||
|
minimumPayment: string | null;
|
||||||
|
name: string;
|
||||||
|
paymentAddress: string;
|
||||||
|
publicKey: string;
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,24 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import debug from 'debug';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
import { Octokit } from 'octokit';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import toml from 'toml';
|
import toml from 'toml';
|
||||||
import debug from 'debug';
|
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||||
|
|
||||||
|
import { Config } from './config';
|
||||||
|
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
||||||
|
import { PackageJSON } from './types';
|
||||||
|
|
||||||
const log = debug('snowball:utils');
|
const log = debug('snowball:utils');
|
||||||
|
|
||||||
export const getConfig = async <ConfigType>(
|
export async function getConfig() {
|
||||||
configFile: string
|
// TODO: get config path using cli
|
||||||
|
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);
|
||||||
@ -19,3 +31,113 @@ export const getConfig = async <ConfigType>(
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkFileExists = async (filePath: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
log(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEntities = async (filePath: string): Promise<any> => {
|
||||||
|
const entitiesData = await fs.readFile(filePath, 'utf-8');
|
||||||
|
const entities = JSON.parse(entitiesData);
|
||||||
|
return entities;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||||
|
entityType: EntityTarget<Entity>,
|
||||||
|
dataSource: DataSource,
|
||||||
|
entities: any,
|
||||||
|
relations?: any | undefined,
|
||||||
|
): Promise<Entity[]> => {
|
||||||
|
const entityRepository = dataSource.getRepository(entityType);
|
||||||
|
|
||||||
|
const savedEntity: Entity[] = [];
|
||||||
|
|
||||||
|
for (const entityData of entities) {
|
||||||
|
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
|
||||||
|
|
||||||
|
if (relations) {
|
||||||
|
for (const field in relations) {
|
||||||
|
const valueIndex = String(field + 'Index');
|
||||||
|
|
||||||
|
entity = {
|
||||||
|
...entity,
|
||||||
|
[field]: relations[field][entityData[valueIndex]],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dbEntity = await entityRepository.save(entity);
|
||||||
|
savedEntity.push(dbEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return savedEntity;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sleep = async (ms: number): Promise<void> =>
|
||||||
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
export const getRepoDetails = async (
|
||||||
|
octokit: Octokit,
|
||||||
|
repository: string,
|
||||||
|
commitHash: string | undefined,
|
||||||
|
): Promise<{
|
||||||
|
repo: string;
|
||||||
|
packageJSON: PackageJSON;
|
||||||
|
repoUrl: string;
|
||||||
|
}> => {
|
||||||
|
const [owner, repo] = repository.split('/');
|
||||||
|
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: 'package.json',
|
||||||
|
ref: commitHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!packageJSONData) {
|
||||||
|
throw new Error('Package.json file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
||||||
|
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
|
||||||
|
|
||||||
|
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||||
|
|
||||||
|
const repoUrl = (
|
||||||
|
await octokit.rest.repos.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
})
|
||||||
|
).data.html_url;
|
||||||
|
|
||||||
|
return {
|
||||||
|
repo,
|
||||||
|
packageJSON,
|
||||||
|
repoUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper method for registry txs to retry once if 'account sequence mismatch' occurs
|
||||||
|
export const registryTransactionWithRetry = async (
|
||||||
|
txMethod: () => Promise<any>
|
||||||
|
): Promise<any> => {
|
||||||
|
try {
|
||||||
|
return await txMethod();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (!error.message.includes('account sequence mismatch')) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`Transaction failed due to account sequence mismatch. Retrying...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await txMethod();
|
||||||
|
} catch (retryError: any) {
|
||||||
|
throw new Error(`Transaction failed again after retry: ${retryError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,8 +2,6 @@ import * as fs from 'fs/promises';
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
||||||
import { getConfig } from '../src/utils';
|
import { getConfig } from '../src/utils';
|
||||||
import { Config } from '../src/config';
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
|
||||||
|
|
||||||
const log = debug('snowball:delete-database');
|
const log = debug('snowball:delete-database');
|
||||||
|
|
||||||
@ -13,9 +11,9 @@ const deleteFile = async (filePath: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const config = await getConfig();
|
||||||
|
|
||||||
deleteFile(config.database.dbPath);
|
deleteFile(config.database.dbPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
main().catch(err => log(err));
|
main().catch((err) => log(err));
|
||||||
|
62
packages/backend/test/fixtures/deployments.json
vendored
62
packages/backend/test/fixtures/deployments.json
vendored
@ -1,14 +1,16 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"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",
|
||||||
@ -16,14 +18,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
@ -31,14 +35,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
@ -48,12 +54,14 @@
|
|||||||
"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",
|
||||||
@ -61,14 +69,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
@ -76,14 +86,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
@ -91,14 +103,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
@ -106,14 +120,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
@ -121,14 +137,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
@ -136,14 +154,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
@ -153,12 +173,14 @@
|
|||||||
"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",
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
||||||
"name": "Snowball Tools",
|
"name": "Deploy Tools",
|
||||||
"slug": "snowball-tools-1"
|
"slug": "deploy-tools"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
|
||||||
"name": "Laconic",
|
|
||||||
"slug": "laconic-2"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -2,77 +2,55 @@
|
|||||||
{
|
{
|
||||||
"memberIndex": 1,
|
"memberIndex": 1,
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 0,
|
"memberIndex": 0,
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 1,
|
"memberIndex": 1,
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 0,
|
"memberIndex": 0,
|
||||||
"projectIndex": 3,
|
"projectIndex": 3,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 3,
|
"projectIndex": 3,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 1,
|
"memberIndex": 1,
|
||||||
"projectIndex": 4,
|
"projectIndex": 4,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 4,
|
"projectIndex": 4,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
10
packages/backend/test/fixtures/projects.json
vendored
10
packages/backend/test/fixtures/projects.json
vendored
@ -10,8 +10,6 @@
|
|||||||
"framework": "test",
|
"framework": "test",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "testProject.snowball.xyz"
|
"subDomain": "testProject.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -25,8 +23,6 @@
|
|||||||
"framework": "test-2",
|
"framework": "test-2",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "testProject-2.snowball.xyz"
|
"subDomain": "testProject-2.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -40,8 +36,6 @@
|
|||||||
"framework": "test-3",
|
"framework": "test-3",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "iglootools.snowball.xyz"
|
"subDomain": "iglootools.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -55,8 +49,6 @@
|
|||||||
"framework": "test-4",
|
"framework": "test-4",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "iglootools-2.snowball.xyz"
|
"subDomain": "iglootools-2.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -70,8 +62,6 @@
|
|||||||
"framework": "test-5",
|
"framework": "test-5",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "snowball-2.snowball.xyz"
|
"subDomain": "snowball-2.snowball.xyz"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
15
packages/backend/test/fixtures/users.json
vendored
15
packages/backend/test/fixtures/users.json
vendored
@ -1,20 +1,23 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
|
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
|
||||||
"name": "Snowball",
|
"name": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
||||||
"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": "Alice Anderson",
|
"name": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
|
||||||
"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": "Bob Banner",
|
"name": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a",
|
||||||
"email": "bob@snowballtools.xyz",
|
"email": "bob@snowballtools.xyz",
|
||||||
"isVerified": true
|
"isVerified": true,
|
||||||
|
"ethAddress": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import * as fs from 'fs/promises';
|
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
@ -11,9 +10,12 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable';
|
|||||||
import { Domain } from '../src/entity/Domain';
|
import { Domain } from '../src/entity/Domain';
|
||||||
import { ProjectMember } from '../src/entity/ProjectMember';
|
import { ProjectMember } from '../src/entity/ProjectMember';
|
||||||
import { Deployment } from '../src/entity/Deployment';
|
import { Deployment } from '../src/entity/Deployment';
|
||||||
import { getConfig } from '../src/utils';
|
import {
|
||||||
import { Config } from '../src/config';
|
checkFileExists,
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
getConfig,
|
||||||
|
getEntities,
|
||||||
|
loadAndSaveData
|
||||||
|
} from '../src/utils';
|
||||||
|
|
||||||
const log = debug('snowball:initialize-database');
|
const log = debug('snowball:initialize-database');
|
||||||
|
|
||||||
@ -27,43 +29,35 @@ const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json';
|
|||||||
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
|
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
|
||||||
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
|
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
|
||||||
|
|
||||||
const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, filePath: string, relations?: any | undefined) => {
|
|
||||||
const entitiesData = await fs.readFile(filePath, 'utf-8');
|
|
||||||
const entities = JSON.parse(entitiesData);
|
|
||||||
const entityRepository = dataSource.getRepository(entityType);
|
|
||||||
|
|
||||||
const savedEntity:Entity[] = [];
|
|
||||||
|
|
||||||
for (const entityData of entities) {
|
|
||||||
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
|
|
||||||
|
|
||||||
if (relations) {
|
|
||||||
for (const field in relations) {
|
|
||||||
const valueIndex = String(field + 'Index');
|
|
||||||
|
|
||||||
entity = {
|
|
||||||
...entity,
|
|
||||||
[field]: relations[field][entityData[valueIndex]]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const dbEntity = await entityRepository.save(entity);
|
|
||||||
savedEntity.push(dbEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return savedEntity;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateTestData = async (dataSource: DataSource) => {
|
const generateTestData = async (dataSource: DataSource) => {
|
||||||
const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH));
|
const userEntities = await getEntities(
|
||||||
const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
path.resolve(__dirname, USER_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
|
||||||
|
|
||||||
|
const orgEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedOrgs = await loadAndSaveData(
|
||||||
|
Organization,
|
||||||
|
dataSource,
|
||||||
|
orgEntities
|
||||||
|
);
|
||||||
|
|
||||||
const projectRelations = {
|
const projectRelations = {
|
||||||
owner: savedUsers,
|
owner: savedUsers,
|
||||||
organization: savedOrgs
|
organization: savedOrgs
|
||||||
};
|
};
|
||||||
|
|
||||||
const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations);
|
const projectEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, PROJECT_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedProjects = await loadAndSaveData(
|
||||||
|
Project,
|
||||||
|
dataSource,
|
||||||
|
projectEntities,
|
||||||
|
projectRelations
|
||||||
|
);
|
||||||
|
|
||||||
const domainRepository = dataSource.getRepository(Domain);
|
const domainRepository = dataSource.getRepository(Domain);
|
||||||
|
|
||||||
@ -71,14 +65,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations);
|
const primaryDomainsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedPrimaryDomains = await loadAndSaveData(
|
||||||
|
Domain,
|
||||||
|
dataSource,
|
||||||
|
primaryDomainsEntities,
|
||||||
|
domainPrimaryRelations
|
||||||
|
);
|
||||||
|
|
||||||
const domainRedirectedRelations = {
|
const domainRedirectedRelations = {
|
||||||
project: savedProjects,
|
project: savedProjects,
|
||||||
redirectTo: savedPrimaryDomains
|
redirectTo: savedPrimaryDomains
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations);
|
const redirectDomainsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
Domain,
|
||||||
|
dataSource,
|
||||||
|
redirectDomainsEntities,
|
||||||
|
domainRedirectedRelations
|
||||||
|
);
|
||||||
|
|
||||||
const savedDomains = await domainRepository.find();
|
const savedDomains = await domainRepository.find();
|
||||||
|
|
||||||
@ -87,14 +97,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
organization: savedOrgs
|
organization: savedOrgs
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations);
|
const userOrganizationsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
UserOrganization,
|
||||||
|
dataSource,
|
||||||
|
userOrganizationsEntities,
|
||||||
|
userOrganizationRelations
|
||||||
|
);
|
||||||
|
|
||||||
const projectMemberRelations = {
|
const projectMemberRelations = {
|
||||||
member: savedUsers,
|
member: savedUsers,
|
||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations);
|
const projectMembersEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
ProjectMember,
|
||||||
|
dataSource,
|
||||||
|
projectMembersEntities,
|
||||||
|
projectMemberRelations
|
||||||
|
);
|
||||||
|
|
||||||
const deploymentRelations = {
|
const deploymentRelations = {
|
||||||
project: savedProjects,
|
project: savedProjects,
|
||||||
@ -102,27 +128,33 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
createdBy: savedUsers
|
createdBy: savedUsers
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations);
|
const deploymentsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
Deployment,
|
||||||
|
dataSource,
|
||||||
|
deploymentsEntities,
|
||||||
|
deploymentRelations
|
||||||
|
);
|
||||||
|
|
||||||
const environmentVariableRelations = {
|
const environmentVariableRelations = {
|
||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations);
|
const environmentVariablesEntities = await getEntities(
|
||||||
};
|
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
|
||||||
|
);
|
||||||
const checkFileExists = async (filePath: string) => {
|
await loadAndSaveData(
|
||||||
try {
|
EnvironmentVariable,
|
||||||
await fs.access(filePath, fs.constants.F_OK);
|
dataSource,
|
||||||
return true;
|
environmentVariablesEntities,
|
||||||
} catch (err) {
|
environmentVariableRelations
|
||||||
log(err);
|
);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const config = await getConfig();
|
||||||
const isDbPresent = await checkFileExists(config.database.dbPath);
|
const isDbPresent = await checkFileExists(config.database.dbPath);
|
||||||
|
|
||||||
if (!isDbPresent) {
|
if (!isDbPresent) {
|
||||||
|
@ -1,32 +1,41 @@
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
||||||
import { Registry } from '@cerc-io/laconic-sdk';
|
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||||
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
|
||||||
import { Config } from '../src/config';
|
|
||||||
import { getConfig } from '../src/utils';
|
import { getConfig } from '../src/utils';
|
||||||
|
|
||||||
const log = debug('snowball:initialize-registry');
|
const log = debug('snowball:initialize-registry');
|
||||||
|
|
||||||
const DENOM = 'aphoton';
|
const DENOM = 'alnt';
|
||||||
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();
|
||||||
|
|
||||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
// TODO: Get authority names from args
|
||||||
|
const authorityNames = ['snowballtools', registryConfig.authority];
|
||||||
|
|
||||||
|
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, {chainId: registryConfig.chainId});
|
||||||
|
|
||||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||||
log('bondId:', bondId);
|
log('bondId:', bondId);
|
||||||
await registry.createBond({ denom: DENOM, amount: BOND_AMOUNT }, registryConfig.privateKey, registryConfig.fee);
|
|
||||||
|
|
||||||
for await (const name of AUTHORITY_NAMES) {
|
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
|
||||||
|
await registry.createBond(
|
||||||
|
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||||
|
registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const name of authorityNames) {
|
||||||
|
await registry.reserveAuthority({ name }, registryConfig.privateKey, fee);
|
||||||
log('Reserved authority name:', name);
|
log('Reserved authority name:', name);
|
||||||
await registry.setAuthorityBond({ name, bondId }, registryConfig.privateKey, registryConfig.fee);
|
await registry.setAuthorityBond(
|
||||||
|
{ name, bondId },
|
||||||
|
registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
);
|
||||||
log(`Bond ${bondId} set for authority ${name}`);
|
log(`Bond ${bondId} set for authority ${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,21 @@ import debug from 'debug';
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { Registry } from '@cerc-io/laconic-sdk';
|
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||||
|
|
||||||
import { Config } from '../src/config';
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH, PROJECT_DOMAIN } from '../src/constants';
|
|
||||||
import { getConfig } from '../src/utils';
|
import { getConfig } from '../src/utils';
|
||||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
|
||||||
|
|
||||||
const log = debug('snowball:publish-deploy-records');
|
const log = debug('snowball:publish-deploy-records');
|
||||||
|
|
||||||
async function main () {
|
async function main() {
|
||||||
const { registryConfig, database } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { registryConfig, database, misc } = await getConfig();
|
||||||
|
|
||||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
const registry = new Registry(
|
||||||
|
registryConfig.gqlEndpoint,
|
||||||
|
registryConfig.restEndpoint,
|
||||||
|
{ chainId: registryConfig.chainId }
|
||||||
|
);
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
const dataSource = new DataSource({
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
@ -36,7 +38,7 @@ async function main () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for await (const deployment of deployments) {
|
for await (const deployment of deployments) {
|
||||||
const url = `${deployment.project.name}-${deployment.id}.${PROJECT_DOMAIN}`;
|
const url = `https://${(deployment.project.name).toLowerCase()}-${deployment.id}.${deployment.deployer.baseDomain}`;
|
||||||
|
|
||||||
const applicationDeploymentRecord = {
|
const applicationDeploymentRecord = {
|
||||||
type: 'ApplicationDeploymentRecord',
|
type: 'ApplicationDeploymentRecord',
|
||||||
@ -53,10 +55,12 @@ async function main () {
|
|||||||
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
|
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
request: deployment.project.applicationDeploymentRequestId,
|
request: deployment.applicationDeploymentRequestId,
|
||||||
url
|
url
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||||
|
|
||||||
const result = await registry.setRecord(
|
const result = await registry.setRecord(
|
||||||
{
|
{
|
||||||
privateKey: registryConfig.privateKey,
|
privateKey: registryConfig.privateKey,
|
||||||
@ -64,11 +68,26 @@ async function main () {
|
|||||||
bondId: registryConfig.bondId
|
bondId: registryConfig.bondId
|
||||||
},
|
},
|
||||||
'',
|
'',
|
||||||
registryConfig.fee
|
fee
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Remove deployment for project subdomain if deployment is for production environment
|
||||||
|
if (deployment.environment === Environment.Production) {
|
||||||
|
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.deployer.baseDomain}`;
|
||||||
|
|
||||||
|
await registry.setRecord(
|
||||||
|
{
|
||||||
|
privateKey: registryConfig.privateKey,
|
||||||
|
record: applicationDeploymentRecord,
|
||||||
|
bondId: registryConfig.bondId
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
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.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
67
packages/backend/test/publish-deployment-removal-records.ts
Normal file
67
packages/backend/test/publish-deployment-removal-records.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import debug from 'debug';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||||
|
|
||||||
|
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 } = await getConfig();
|
||||||
|
|
||||||
|
const registry = new Registry(
|
||||||
|
registryConfig.gqlEndpoint,
|
||||||
|
registryConfig.restEndpoint,
|
||||||
|
{ chainId: 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 fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||||
|
|
||||||
|
const result = await registry.setRecord(
|
||||||
|
{
|
||||||
|
privateKey: registryConfig.privateKey,
|
||||||
|
record: applicationDeploymentRemovalRecord,
|
||||||
|
bondId: registryConfig.bondId
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
fee
|
||||||
|
);
|
||||||
|
|
||||||
|
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
|
||||||
|
log(`Application deployment removal record published: ${result.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
log(err);
|
||||||
|
});
|
3
packages/deployer/.env.example
Normal file
3
packages/deployer/.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
REGISTRY_BOND_ID=
|
||||||
|
DEPLOYER_LRN=
|
||||||
|
AUTHORITY=
|
1
packages/deployer/.gitignore
vendored
Normal file
1
packages/deployer/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
records/*.yml
|
64
packages/deployer/README.md
Normal file
64
packages/deployer/README.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# 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`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy-frontend.staging.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- To deploy frontend app to `dashboard.apps.snowballtools.com`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./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 registry account get
|
||||||
|
|
||||||
|
# Bond balance
|
||||||
|
yarn laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||||
|
```
|
||||||
|
|
||||||
|
- Command to refill bond
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000
|
||||||
|
```
|
10
packages/deployer/config.staging.yml
Normal file
10
packages/deployer/config.staging.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
registry:
|
||||||
|
restEndpoint: 'http://console.laconic.com:1317'
|
||||||
|
gqlEndpoint: 'http://console.laconic.com:9473/api'
|
||||||
|
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
|
||||||
|
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
|
||||||
|
chainId: laconic_9000-1
|
||||||
|
gas:
|
||||||
|
fees:
|
||||||
|
gasPrice: 1
|
8
packages/deployer/config.yml
Normal file
8
packages/deployer/config.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
registry:
|
||||||
|
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||||
|
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||||
|
userKey:
|
||||||
|
bondId:
|
||||||
|
chainId: laconic-testnet-2
|
||||||
|
gasPrice: 0.001alnt
|
156
packages/deployer/deploy-frontend.sh
Executable file
156
packages/deployer/deploy-frontend.sh
Executable file
@ -0,0 +1,156 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
source .env
|
||||||
|
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||||
|
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||||
|
echo "Using AUTHORITY: $AUTHORITY"
|
||||||
|
|
||||||
|
# Repository URL
|
||||||
|
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
|
|
||||||
|
# Get the latest commit hash for a branch
|
||||||
|
BRANCH_NAME="main"
|
||||||
|
LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}')
|
||||||
|
|
||||||
|
echo "Latest commit hash for branch $BRANCH_NAME: $LATEST_HASH"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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 registry record list --type ApplicationRecord --all --name "deploy-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-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: deploy-frontend
|
||||||
|
app_version: $PACKAGE_VERSION
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Files generated successfully"
|
||||||
|
|
||||||
|
RECORD_FILE=records/application-record.yml
|
||||||
|
|
||||||
|
# Publish ApplicationRecord
|
||||||
|
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to publish record"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
||||||
|
echo "ApplicationRecord published"
|
||||||
|
echo $RECORD_ID
|
||||||
|
|
||||||
|
# Set name to record
|
||||||
|
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set hash"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
# Set name if latest release
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set release"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||||
|
|
||||||
|
# Check if record found for REGISTRY_APP_LRN
|
||||||
|
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to query name"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||||
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
|
echo "No record found for $REGISTRY_APP_LRN."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get payment address for deployer
|
||||||
|
paymentAddress=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
|
||||||
|
paymentAmount=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
|
||||||
|
|
||||||
|
echo "Paying address: $paymentAddress with amount $paymentAmount..."
|
||||||
|
|
||||||
|
# Pay deployer if paymentAmount is not null
|
||||||
|
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
|
||||||
|
payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")
|
||||||
|
|
||||||
|
# Extract the transaction hash
|
||||||
|
txHash=$(echo "$payment" | jq -r '.tx.hash')
|
||||||
|
echo "Paid deployer with txHash as $txHash"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Payment amount is null; skipping payment."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate application-deployment-request.yml
|
||||||
|
cat >./records/application-deployment-request.yml <<EOF
|
||||||
|
record:
|
||||||
|
type: ApplicationDeploymentRequest
|
||||||
|
version: '1.0.0'
|
||||||
|
name: deploy-frontend@$PACKAGE_VERSION
|
||||||
|
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
||||||
|
deployer: $DEPLOYER_LRN
|
||||||
|
dns: deploy.laconic.com
|
||||||
|
config:
|
||||||
|
env:
|
||||||
|
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.laconic.com
|
||||||
|
LACONIC_HOSTED_CONFIG_github_clientid: Ov23li4NtYybQlF6u5Dk
|
||||||
|
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||||
|
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||||
|
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
||||||
|
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
||||||
|
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||||
|
LACONIC_HOSTED_CONFIG_wallet_iframe_url: https://wallet.laconic.com
|
||||||
|
meta:
|
||||||
|
note: Added @ $CURRENT_DATE_TIME
|
||||||
|
repository: "$REPO_URL"
|
||||||
|
repository_ref: $LATEST_HASH
|
||||||
|
payment: $txHash
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RECORD_FILE=records/application-deployment-request.yml
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to query deployment request"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
|
||||||
|
echo "ApplicationDeploymentRequest published"
|
||||||
|
echo $DEPLOYMENT_REQUEST_ID
|
149
packages/deployer/deploy-frontend.vaasl.sh
Executable file
149
packages/deployer/deploy-frontend.vaasl.sh
Executable file
@ -0,0 +1,149 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
source .env
|
||||||
|
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||||
|
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||||
|
echo "Using AUTHORITY: $AUTHORITY"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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 registry record list --type ApplicationRecord --all --name "deploy-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-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: deploy-frontend
|
||||||
|
app_version: $PACKAGE_VERSION
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Files generated successfully"
|
||||||
|
|
||||||
|
RECORD_FILE=records/application-record.yml
|
||||||
|
|
||||||
|
# Publish ApplicationRecord
|
||||||
|
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to publish record"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
||||||
|
echo "ApplicationRecord published"
|
||||||
|
echo $RECORD_ID
|
||||||
|
|
||||||
|
# Set name to record
|
||||||
|
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set hash"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
# Set name if latest release
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set release"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||||
|
|
||||||
|
# Check if record found for REGISTRY_APP_LRN
|
||||||
|
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to query name"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||||
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
|
echo "No record found for $REGISTRY_APP_LRN."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get payment address for deployer
|
||||||
|
paymentAddress=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
|
||||||
|
paymentAmount=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
|
||||||
|
# Pay deployer if paymentAmount is not null
|
||||||
|
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
|
||||||
|
payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")
|
||||||
|
|
||||||
|
# Extract the transaction hash
|
||||||
|
txHash=$(echo "$payment" | jq -r '.tx.hash')
|
||||||
|
echo "Paid deployer with txHash as $txHash"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Payment amount is null; skipping payment."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate application-deployment-request.yml
|
||||||
|
cat >./records/application-deployment-request.yml <<EOF
|
||||||
|
record:
|
||||||
|
type: ApplicationDeploymentRequest
|
||||||
|
version: '1.0.0'
|
||||||
|
name: deploy-frontend@$PACKAGE_VERSION
|
||||||
|
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
||||||
|
deployer: $DEPLOYER_LRN
|
||||||
|
dns: deploy
|
||||||
|
config:
|
||||||
|
env:
|
||||||
|
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
|
||||||
|
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
|
||||||
|
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||||
|
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||||
|
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
||||||
|
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
||||||
|
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||||
|
meta:
|
||||||
|
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||||
|
repository: "$REPO_URL"
|
||||||
|
repository_ref: $LATEST_HASH
|
||||||
|
payment: $txHash
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RECORD_FILE=records/application-deployment-request.yml
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to query deployment request"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
|
||||||
|
echo "ApplicationDeploymentRequest published"
|
||||||
|
echo $DEPLOYMENT_REQUEST_ID
|
9
packages/deployer/package.json
Normal file
9
packages/deployer/package.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "deployer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@cerc-io/laconic-registry-cli": "^0.2.9"
|
||||||
|
}
|
||||||
|
}
|
0
packages/deployer/records/.gitkeep
Normal file
0
packages/deployer/records/.gitkeep
Normal file
56
packages/deployer/remove-deployment.sh
Executable file
56
packages/deployer/remove-deployment.sh
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
source .env
|
||||||
|
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||||
|
|
||||||
|
# Generate application-deployment-removal-request.yml
|
||||||
|
REMOVAL_REQUEST_RECORD_FILE=records/application-deployment-removal-request.yml
|
||||||
|
# TODO: Pass deployment record ID as arg
|
||||||
|
DEPLOYMENT_RECORD_ID=bafyreidjho77xeczaqpyawhc4wbpm5it5atibtuxk6ost6vnpu2svlp3ka
|
||||||
|
|
||||||
|
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
|
||||||
|
record:
|
||||||
|
deployer: $DEPLOYER_LRN
|
||||||
|
deployment: $DEPLOYMENT_RECORD_ID
|
||||||
|
type: ApplicationDeploymentRemovalRequest
|
||||||
|
version: 1.0.0
|
||||||
|
EOF
|
||||||
|
|
||||||
|
CONFIG_FILE=config.yml
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||||
|
echo "ApplicationDeploymentRemovalRequest published"
|
||||||
|
echo $REMOVAL_REQUEST_ID
|
||||||
|
|
||||||
|
# Deployment checks
|
||||||
|
RETRY_INTERVAL=30
|
||||||
|
MAX_RETRIES=20
|
||||||
|
|
||||||
|
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||||
|
retry_count=0
|
||||||
|
while true; do
|
||||||
|
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry 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
|
||||||
|
|
||||||
|
echo "Deployment removal successful"
|
23
packages/deployer/test/README.md
Normal file
23
packages/deployer/test/README.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# deployer test
|
||||||
|
|
||||||
|
Check if the live web app deployer is in a working state
|
||||||
|
|
||||||
|
- Web app repo used: <https://github.com/snowball-tools/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
|
@ -0,0 +1,4 @@
|
|||||||
|
record:
|
||||||
|
deployment: <APPLICATION_DEPLOYMENT_RECORD_ID>
|
||||||
|
type: ApplicationDeploymentRemovalRequest
|
||||||
|
version: 1.0.0
|
@ -0,0 +1,15 @@
|
|||||||
|
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/test-progressive-web-app"
|
||||||
|
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
8
packages/deployer/test/records/application-record.yml
Normal file
8
packages/deployer/test/records/application-record.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
record:
|
||||||
|
type: ApplicationRecord
|
||||||
|
version: 0.0.1
|
||||||
|
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
||||||
|
repository: ["https://github.com/snowball-tools/test-progressive-web-app"]
|
||||||
|
app_type: webapp
|
||||||
|
name: deployment-test-app
|
||||||
|
app_version: 0.1.24
|
225
packages/deployer/test/test-webapp-deployment-undeployment.sh
Executable file
225
packages/deployer/test/test-webapp-deployment-undeployment.sh
Executable file
@ -0,0 +1,225 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Repository URL
|
||||||
|
REPO_URL="https://github.com/snowball-tools/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 registry 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: lrn://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 registry record publish --filename $RECORD_FILE | jq -r '.id')
|
||||||
|
echo "ApplicationRecord published"
|
||||||
|
echo $RECORD_ID
|
||||||
|
|
||||||
|
# Set name to record
|
||||||
|
REGISTRY_APP_LRN="lrn://snowballtools/applications/$APP_NAME"
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||||
|
sleep 2
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||||
|
sleep 2
|
||||||
|
# Set name if latest release
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||||
|
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||||
|
|
||||||
|
# Check if record exists for REGISTRY_APP_LRN
|
||||||
|
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN" | jq '.[0]')
|
||||||
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
|
echo "No record found for $REGISTRY_APP_LRN."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry 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 registry 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=10
|
||||||
|
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 registry 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 registry 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"
|
@ -1,4 +0,0 @@
|
|||||||
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
|
|
||||||
|
|
||||||
REACT_APP_GITHUB_CLIENT_ID =
|
|
||||||
REACT_APP_GITHUB_TEMPLATE_REPO =
|
|
18
packages/frontend/.env.example
Normal file
18
packages/frontend/.env.example
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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_GITHUB_NEXT_APP_TEMPLATE_REPO="snowball-tools/starter.nextjs-react-tailwind"
|
||||||
|
|
||||||
|
VITE_WALLET_CONNECT_ID=
|
||||||
|
|
||||||
|
VITE_LIT_RELAY_API_KEY=
|
||||||
|
|
||||||
|
VITE_BUGSNAG_API_KEY=
|
||||||
|
|
||||||
|
VITE_PASSKEY_WALLET_RPID=
|
||||||
|
VITE_TURNKEY_API_BASE_URL=
|
||||||
|
|
||||||
|
VITE_LACONICD_CHAIN_ID=
|
||||||
|
VITE_WALLET_IFRAME_URL=
|
@ -1 +0,0 @@
|
|||||||
build
|
|
19
packages/frontend/.eslintrc.cjs
Normal file
19
packages/frontend/.eslintrc.cjs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"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
packages/frontend/.gitignore
vendored
2
packages/frontend/.gitignore
vendored
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
@ -21,3 +22,4 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
*storybook.log
|
1
packages/frontend/.node-version
Normal file
1
packages/frontend/.node-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
v20.12.1
|
@ -1,3 +1 @@
|
|||||||
# artifacts
|
dist/
|
||||||
build
|
|
||||||
coverage
|
|
33
packages/frontend/.storybook/main.ts
Normal file
33
packages/frontend/.storybook/main.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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',
|
||||||
|
},
|
||||||
|
staticDirs: ['../public'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
16
packages/frontend/.storybook/preview.ts
Normal file
16
packages/frontend/.storybook/preview.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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
packages/frontend/.vscode/settings.json
vendored
39
packages/frontend/.vscode/settings.json
vendored
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
// 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\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,46 +1,63 @@
|
|||||||
# Getting Started with Create React App
|
# frontend
|
||||||
|
|
||||||
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.
|
This is a [vite](https://vitejs.dev/) [react](https://reactjs.org/) [nextjs](https://nextjs.org/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
|
||||||
|
|
||||||
## Available Scripts
|
## Getting Started
|
||||||
|
|
||||||
In the project directory, you can run:
|
### Install dependencies
|
||||||
|
|
||||||
### `yarn start`
|
In the root of the project, run:
|
||||||
|
|
||||||
Runs the app in the development mode.\
|
```zsh
|
||||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
The page will reload if you make edits.\
|
### Build backend
|
||||||
You will also see any lint errors in the console.
|
|
||||||
|
|
||||||
### `yarn test`
|
```zsh
|
||||||
|
yarn build --ignore frontend
|
||||||
|
```
|
||||||
|
|
||||||
Launches the test runner in the interactive watch mode.\
|
### Environment variables
|
||||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
|
||||||
|
|
||||||
### `yarn build`
|
#### Local
|
||||||
|
|
||||||
Builds the app for production to the `build` folder.\
|
Copy the `.env.example` file to `.env`:
|
||||||
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.\
|
```zsh
|
||||||
Your app is ready to be deployed!
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
#### Staging environment variables
|
||||||
|
|
||||||
### `yarn eject`
|
Change in [deployer/deploy-frontend.staging.sh](/packages/deployer/deploy-frontend.staging.sh)
|
||||||
|
|
||||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
#### Production environment variables
|
||||||
|
|
||||||
If you aren’t 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.
|
Change in [deployer/deploy-frontend.sh](/packages/deployer/deploy-frontend.sh)
|
||||||
|
|
||||||
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 you’re on your own.
|
### Run development server
|
||||||
|
|
||||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
```zsh
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
## Learn More
|
## Deployment
|
||||||
|
|
||||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
From the root of the project,
|
||||||
|
|
||||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
### Staging
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
cd packages/deployer && ./deploy-frontend.staging.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
cd packages/deployer && ./deploy-frontend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment status
|
||||||
|
|
||||||
|
Check the status of the deployment [here](https://webapp-deployer.apps.snowballtools.com)
|
||||||
|
4
packages/frontend/chromatic.config.json
Normal file
4
packages/frontend/chromatic.config.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"projectId": "Project:663d04870db27ed66a48e466",
|
||||||
|
"zip": true
|
||||||
|
}
|
24
packages/frontend/index.html
Normal file
24
packages/frontend/index.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark dark:bg-background dark:text-foreground">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content="laconic tools dashboard" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<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>Laconic</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,26 +1,57 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"version": "1.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",
|
||||||
|
"@emotion/react": "^11.13.3",
|
||||||
|
"@emotion/styled": "^11.13.0",
|
||||||
|
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||||
"@fontsource/inter": "^5.0.16",
|
"@fontsource/inter": "^5.0.16",
|
||||||
"@material-tailwind/react": "^2.1.7",
|
"@mui/material": "^6.1.3",
|
||||||
"@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/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",
|
||||||
"@types/jest": "^27.5.2",
|
"@turnkey/http": "^2.10.0",
|
||||||
"@types/node": "^16.18.68",
|
"@turnkey/sdk-react": "^0.1.0",
|
||||||
"@types/react": "^18.2.42",
|
"@turnkey/webauthn-stamper": "^0.5.0",
|
||||||
"@types/react-dom": "^18.2.17",
|
"@walletconnect/ethereum-provider": "^2.16.1",
|
||||||
|
"@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",
|
"clsx": "^2.1.0",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
"downshift": "^8.2.3",
|
"downshift": "^8.3.2",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"framer-motion": "^11.0.8",
|
||||||
"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",
|
||||||
@ -30,54 +61,44 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropdown": "^1.11.0",
|
"react-dropdown": "^1.11.0",
|
||||||
"react-hook-form": "^7.49.0",
|
"react-hook-form": "^7.49.0",
|
||||||
"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",
|
||||||
"typescript": "^4.9.5",
|
"usehooks-ts": "^2.15.1",
|
||||||
"usehooks-ts": "^2.10.0",
|
"uuid": "^9.0.1",
|
||||||
"vertical-stepper-nav": "^1.0.2",
|
"viem": "^2.7.11",
|
||||||
|
"wagmi": "2.5.7",
|
||||||
"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": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@chromatic-com/storybook": "^1.3.3",
|
||||||
|
"@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",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
"@types/node": "^16.18.68",
|
||||||
"@typescript-eslint/parser": "^6.13.2",
|
"@types/react": "^18.2.66",
|
||||||
"eslint": "^8.55.0",
|
"@types/react-dom": "^18.2.22",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"@types/uuid": "^9.0.8",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"autoprefixer": "^10.4.19",
|
||||||
|
"chromatic": "^11.3.2",
|
||||||
|
"eslint-plugin-storybook": "^0.8.0",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.1.0",
|
||||||
"tailwindcss": "^3.4.1"
|
"storybook": "^8.0.10",
|
||||||
|
"storybook-addon-remix-react-router": "^3.0.0",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
packages/frontend/postcss.config.js
Normal file
6
packages/frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
1
packages/frontend/public/.well-known/walletconnect.txt
Normal file
1
packages/frontend/public/.well-known/walletconnect.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
350e9ac2-8b27-4a79-9a82-78cfdb68ef71=0eacb7ae462f82c8b0199d28193b0bfa5265973dbb1fe991eec2cab737dfc1ec
|
9
packages/frontend/public/browserconfig.xml
Normal file
9
packages/frontend/public/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#2d89ef</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
3
packages/frontend/public/dot-border-line.svg
Normal file
3
packages/frontend/public/dot-border-line.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 196 B |
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user