diff --git a/ex b/ex index 9009206..1878870 100755 --- a/ex +++ b/ex @@ -125,6 +125,8 @@ if [ ! -d "./node_modules" ]; then echoRun "$ENV_PNPM" i + echoRun ./node_modules/.bin/ts-patch i + if [ ! -f "./.env" ]; then echoRun cp example.env .env fi diff --git a/package.json b/package.json index e35ba5f..03e7e39 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,10 @@ "lib": "lib" }, "dependencies": { - "@extollo/lib": "^0.9.31", + "@extollo/lib": "^0.9.32", "copyfiles": "^2.4.1", "feed": "^4.2.2", + "gotify": "^1.1.0", "rimraf": "^3.0.2", "ts-expose-internals": "^4.5.4", "ts-patch": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 537bd94..8367547 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2,9 +2,10 @@ lockfileVersion: 5.3 specifiers: '@extollo/cc': ^0.6.0 - '@extollo/lib': ^0.9.31 + '@extollo/lib': ^0.9.32 copyfiles: ^2.4.1 feed: ^4.2.2 + gotify: ^1.1.0 rimraf: ^3.0.2 ts-expose-internals: ^4.5.4 ts-patch: ^2.0.1 @@ -13,9 +14,10 @@ specifiers: zod: ^3.11.6 dependencies: - '@extollo/lib': 0.9.31 + '@extollo/lib': 0.9.32 copyfiles: 2.4.1 feed: 4.2.2 + gotify: 1.1.0 rimraf: 3.0.2 ts-expose-internals: 4.5.4 ts-patch: 2.0.1_typescript@4.3.2 @@ -112,8 +114,8 @@ packages: - supports-color dev: true - /@extollo/lib/0.9.31: - resolution: {integrity: sha512-sYtqWTL+hmkPZ44fwWdRsHK1081m98547r9snFm8i+ou13Npw3i4RBf8k+uFRUCayMM4PKVWqgo0bZJLvO4W1g==} + /@extollo/lib/0.9.32: + resolution: {integrity: sha512-3DuRrLFmYY6w0rK5QKSlNWbGZnel6Lj2vjSGwwbj1IEJXGcZSA+TMXLghAwPkKyUsyX0WwdXVn6S51jmRHyWJw==} dependencies: '@atao60/fse-cli': 0.1.7 '@extollo/ui': 0.1.0_@types+node@14.18.12 @@ -305,6 +307,18 @@ packages: resolution: {integrity: sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==} dev: false + /@sindresorhus/is/2.1.1: + resolution: {integrity: sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg==} + engines: {node: '>=10'} + dev: false + + /@szmarczak/http-timer/4.0.6: + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + dependencies: + defer-to-connect: 2.0.1 + dev: false + /@tsconfig/node10/1.0.8: resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==} @@ -333,6 +347,15 @@ packages: '@types/node': 17.0.23 dev: false + /@types/cacheable-request/6.0.2: + resolution: {integrity: sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==} + dependencies: + '@types/http-cache-semantics': 4.0.1 + '@types/keyv': 3.1.4 + '@types/node': 17.0.23 + '@types/responselike': 1.0.0 + dev: false + /@types/cli-color/2.0.2: resolution: {integrity: sha512-1ErQIcmNHtNViGKTtB/TIKqMkC2RkKI2nBneCr9hSCPo9H05g9VzjlaXPW3H0vaI8zFGjJZvSav+VKDKCtKgKA==} dev: true @@ -353,18 +376,32 @@ packages: '@types/minimatch': 3.0.5 '@types/node': 17.0.23 + /@types/http-cache-semantics/4.0.1: + resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} + dev: false + /@types/ioredis/4.28.10: resolution: {integrity: sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==} dependencies: '@types/node': 17.0.23 dev: false + /@types/json-buffer/3.0.0: + resolution: {integrity: sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ==} + dev: false + /@types/jsonwebtoken/8.5.8: resolution: {integrity: sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==} dependencies: '@types/node': 17.0.23 dev: false + /@types/keyv/3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + dependencies: + '@types/node': 17.0.23 + dev: false + /@types/mime-types/2.1.1: resolution: {integrity: sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==} dev: false @@ -404,6 +441,12 @@ packages: resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} dev: false + /@types/responselike/1.0.0: + resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} + dependencies: + '@types/node': 17.0.23 + dev: false + /@types/rimraf/3.0.2: resolution: {integrity: sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==} dependencies: @@ -638,6 +681,27 @@ packages: dicer: 0.3.0 dev: false + /cacheable-lookup/2.0.1: + resolution: {integrity: sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg==} + engines: {node: '>=10'} + dependencies: + '@types/keyv': 3.1.4 + keyv: 4.2.1 + dev: false + + /cacheable-request/7.0.2: + resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.2 + get-stream: 5.2.0 + http-cache-semantics: 4.1.0 + keyv: 4.2.1 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.0 + dev: false + /call-bind/1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -734,6 +798,12 @@ packages: wrap-ansi: 7.0.0 dev: false + /clone-response/1.0.2: + resolution: {integrity: sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=} + dependencies: + mimic-response: 1.0.1 + dev: false + /clone/1.0.4: resolution: {integrity: sha1-2jCcwmPfFZlMaIypAheco8fNfH4=} engines: {node: '>=0.8'} @@ -771,6 +841,14 @@ packages: resolution: {integrity: sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==} dev: false + /compress-brotli/1.3.6: + resolution: {integrity: sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==} + engines: {node: '>= 12'} + dependencies: + '@types/json-buffer': 3.0.0 + json-buffer: 3.0.1 + dev: false + /concat-map/0.0.1: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} @@ -843,11 +921,23 @@ packages: dependencies: ms: 2.1.2 + /decompress-response/5.0.0: + resolution: {integrity: sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw==} + engines: {node: '>=10'} + dependencies: + mimic-response: 2.1.0 + dev: false + /defaults/1.0.3: resolution: {integrity: sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=} dependencies: clone: 1.0.4 + /defer-to-connect/2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: false + /delegates/1.0.0: resolution: {integrity: sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=} dev: false @@ -892,6 +982,10 @@ packages: engines: {node: '>=10'} dev: false + /duplexer3/0.1.4: + resolution: {integrity: sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=} + dev: false + /ecdsa-sig-formatter/1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: @@ -901,6 +995,12 @@ packages: /emoji-regex/8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + /end-of-stream/1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: false + /es5-ext/0.10.59: resolution: {integrity: sha512-cOgyhW0tIJyQY1Kfw6Kr0viu9ZlUctVchRMZ7R0HiH3dxTSp5zJDLecwxUqPUrGKMsgBI1wd1FL+d9Jxfi4cLw==} engines: {node: '>=0.10'} @@ -1096,6 +1196,13 @@ packages: has-symbols: 1.0.3 dev: false + /get-stream/5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: false + /glob-parent/5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1131,6 +1238,33 @@ packages: merge2: 1.4.1 slash: 3.0.0 + /got/10.7.0: + resolution: {integrity: sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg==} + engines: {node: '>=10'} + dependencies: + '@sindresorhus/is': 2.1.1 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.2 + cacheable-lookup: 2.0.1 + cacheable-request: 7.0.2 + decompress-response: 5.0.0 + duplexer3: 0.1.4 + get-stream: 5.2.0 + lowercase-keys: 2.0.0 + mimic-response: 2.1.0 + p-cancelable: 2.1.1 + p-event: 4.2.0 + responselike: 2.0.0 + to-readable-stream: 2.1.0 + type-fest: 0.10.0 + dev: false + + /gotify/1.1.0: + resolution: {integrity: sha512-f3PUh08i+1JCJuzv9CRYuIx1QOb9DoWXRvWaPttgiZEG7XtetDQVv6S9cr6suPNKkZI4ry8dqdU2qdb9AiPinw==} + dependencies: + got: 10.7.0 + dev: false + /graceful-fs/4.2.9: resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==} @@ -1173,6 +1307,10 @@ packages: dependencies: function-bind: 1.1.1 + /http-cache-semantics/4.1.0: + resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} + dev: false + /https-proxy-agent/5.0.0: resolution: {integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==} engines: {node: '>= 6'} @@ -1339,6 +1477,10 @@ packages: resolution: {integrity: sha1-Fzb939lyTyijaCrcYjCufk6Weds=} dev: false + /json-buffer/3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: false + /jsonc-parser/3.0.0: resolution: {integrity: sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==} dev: false @@ -1393,6 +1535,13 @@ packages: safe-buffer: 5.1.2 dev: false + /keyv/4.2.1: + resolution: {integrity: sha512-cAJq5cTfxQdq1DHZEVNpnk4mEvhP+8UP8UQftLtTtJ98beKkRHf+62M0mIDM2u/IWXyP8bmGB375/6uGdSX2MA==} + dependencies: + compress-brotli: 1.3.6 + json-buffer: 3.0.1 + dev: false + /kind-of/6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -1447,6 +1596,11 @@ packages: chalk: 4.1.2 is-unicode-supported: 0.1.0 + /lowercase-keys/2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: false + /lru-cache/6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -1523,6 +1677,16 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + /mimic-response/1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: false + + /mimic-response/2.1.0: + resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} + engines: {node: '>=8'} + dev: false + /minimatch/3.0.4: resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} dependencies: @@ -1624,6 +1788,11 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + /normalize-url/6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: false + /npmlog/5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} dependencies: @@ -1670,11 +1839,35 @@ packages: resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=} engines: {node: '>=0.10.0'} + /p-cancelable/2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + dev: false + + /p-event/4.2.0: + resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==} + engines: {node: '>=8'} + dependencies: + p-timeout: 3.2.0 + dev: false + + /p-finally/1.0.0: + resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=} + engines: {node: '>=4'} + dev: false + /p-map/2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} dev: false + /p-timeout/3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + dependencies: + p-finally: 1.0.0 + dev: false + /packet-reader/1.0.0: resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} dev: false @@ -1888,6 +2081,13 @@ packages: pug-strip-comments: 2.0.0 dev: false + /pump/3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: false + /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1969,6 +2169,12 @@ packages: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + /responselike/2.0.0: + resolution: {integrity: sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==} + dependencies: + lowercase-keys: 2.0.0 + dev: false + /restore-cursor/3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -2221,6 +2427,11 @@ packages: engines: {node: '>=4'} dev: false + /to-readable-stream/2.1.0: + resolution: {integrity: sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w==} + engines: {node: '>=8'} + dev: false + /to-regex-range/5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2393,6 +2604,11 @@ packages: resolution: {integrity: sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=} dev: false + /type-fest/0.10.0: + resolution: {integrity: sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw==} + engines: {node: '>=8'} + dev: false + /type-fest/0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} diff --git a/src/app/AppUnit.ts b/src/app/AppUnit.ts index d31ca04..456dacf 100644 --- a/src/app/AppUnit.ts +++ b/src/app/AppUnit.ts @@ -1,9 +1,19 @@ -import {ORMUser, Singleton, Unit} from '@extollo/lib' +import {Config, Inject, ORMUser, Singleton, Unit} from '@extollo/lib' import {User} from './models/User.model' +import {Gotify} from 'gotify' @Singleton() export class AppUnit extends Unit { + @Inject() + protected readonly config!: Config + async up(): Promise { this.container().registerStaticOverride(ORMUser, User) + + const gotify = new Gotify({ + server: this.config.safe('gotify.server').string(), + }) + + this.container().registerSingletonInstance(Gotify, gotify) } } diff --git a/src/app/configs/gotify.config.ts b/src/app/configs/gotify.config.ts new file mode 100644 index 0000000..bd3addc --- /dev/null +++ b/src/app/configs/gotify.config.ts @@ -0,0 +1,6 @@ +import { env } from '@extollo/lib' + +export default { + server: env('GOTIFY_SERVER'), + app: env('GOTIFY_TOKEN'), +} diff --git a/src/app/http/controllers/Home.controller.ts b/src/app/http/controllers/Home.controller.ts index 44c7f7d..2978a5c 100644 --- a/src/app/http/controllers/Home.controller.ts +++ b/src/app/http/controllers/Home.controller.ts @@ -1,6 +1,22 @@ -import {Controller, view, Injectable, SecurityContext, Inject, Collection, Config, Routing, file, Application, plaintext} from '@extollo/lib' +import { + Controller, + view, + Injectable, + SecurityContext, + Inject, + Collection, + Config, + Routing, + file, + Application, + make, + Valid, +} from '@extollo/lib' import {WorkItem} from '../../models/WorkItem.model' import {FeedPost} from '../../models/FeedPost.model' +import {ContactForm} from '../../types/ContactForm.type' +import {ContactSubmission} from '../../models/ContactSubmission.model' +import {Gotify} from 'gotify' @Injectable() export class Home extends Controller { @@ -13,6 +29,9 @@ export class Home extends Controller { @Inject() protected readonly routing!: Routing + @Inject() + protected readonly gotify!: Gotify + public async welcome(feedPosts: Collection) { const workItems = await this.getWorkItems() @@ -88,4 +107,29 @@ export class Home extends Controller { .appPath('resources', 'assets', 'humans.txt') .read() } + + async contact(data: Valid) { + const submission = make(ContactSubmission) + submission.name = data.name + submission.email = data.email + submission.message = data.message + await submission.save() + + this.gotify.send({ + app: this.config.get('gotify.app'), + title: `Contact form submission from ${data.name}`, + message: [ + `From: ${data.name}`, + `E-mail: ${data.email}`, + 'Message:', + data.message, + ].join('\n'), + }) + + return view('message', { + title: 'Message Sent', + message: 'Your message has been sent. Thanks! I\'ll be in touch soon.', + buttonAction: this.routing.getNamedPath('home').toRemote, + }) + } } diff --git a/src/app/http/middlewares/parameters/ValidContactForm.middleware.ts b/src/app/http/middlewares/parameters/ValidContactForm.middleware.ts new file mode 100644 index 0000000..b7021a4 --- /dev/null +++ b/src/app/http/middlewares/parameters/ValidContactForm.middleware.ts @@ -0,0 +1,15 @@ +import {ParameterMiddleware, Injectable, Either, ResponseObject, Validator, Valid, right} from '@extollo/lib' +import {ContactForm} from '../../../types/ContactForm.type' + +/** + * ContactForm Middleware + * -------------------------------------------- + * Parse the contact form data and validate it. Provide the fields as middleware. + */ +@Injectable() +export class ValidContactForm extends ParameterMiddleware> { + async handle(): Promise>> { + const validator = new Validator() + return right(validator.parse(this.request.input())) + } +} diff --git a/src/app/http/routes/app.routes.ts b/src/app/http/routes/app.routes.ts index db03456..4bfb3ab 100644 --- a/src/app/http/routes/app.routes.ts +++ b/src/app/http/routes/app.routes.ts @@ -6,6 +6,7 @@ import {GoLinks} from '../controllers/GoLinks.controller' import {Feed} from '../controllers/Feed.controller' import {LoadSnippet} from '../middlewares/parameters/LoadSnippet.middleware' import {LoadFeedPosts} from '../middlewares/parameters/LoadFeedPosts.middleware' +import {ValidContactForm} from '../middlewares/parameters/ValidContactForm.middleware' Route .group('/', () => { @@ -14,6 +15,11 @@ Route .calls(Home, home => home.welcome) .alias('home') + Route.post('/contact') + .parameterMiddleware(ValidContactForm) + .calls(Home, home => home.contact) + .alias('contact') + Route.get('/humans.txt') .calls(Home, home => home.humans) diff --git a/src/app/migrations/2022-04-05T18:38:54.681Z_CreateContactSubmissionsTableMigration.migration.ts b/src/app/migrations/2022-04-05T18:38:54.681Z_CreateContactSubmissionsTableMigration.migration.ts new file mode 100644 index 0000000..988a900 --- /dev/null +++ b/src/app/migrations/2022-04-05T18:38:54.681Z_CreateContactSubmissionsTableMigration.migration.ts @@ -0,0 +1,51 @@ +import {Injectable, Migration, Inject, DatabaseService, FieldType, raw} from '@extollo/lib' + +/** + * CreateContactSubmissionsTableMigration + * ---------------------------------- + * Put some description here. + */ +@Injectable() +export default class CreateContactSubmissionsTableMigration extends Migration { + @Inject() + protected readonly db!: DatabaseService + + /** + * Apply the migration. + */ + async up(): Promise { + const schema = this.db.get().schema() + const table = await schema.table('contact_submissions') + + table.primaryKey('contact_submission_id').required() + + table.column('email') + .type(FieldType.varchar) + .required() + + table.column('name') + .type(FieldType.varchar) + .required() + + table.column('message') + .type(FieldType.text) + + table.column('sent_at') + .type(FieldType.timestamp) + .default(raw('NOW()')) + + await schema.commit(table) + } + + /** + * Undo the migration. + */ + async down(): Promise { + const schema = this.db.get().schema() + const table = await schema.table('contact_submissions') + + table.dropIfExists() + + await schema.commit(table) + } +} diff --git a/src/app/models/ContactSubmission.model.ts b/src/app/models/ContactSubmission.model.ts new file mode 100644 index 0000000..21eb809 --- /dev/null +++ b/src/app/models/ContactSubmission.model.ts @@ -0,0 +1,27 @@ +import {Field, FieldType, Injectable, Model} from '@extollo/lib' + +/** + * ContactSubmission Model + * ----------------------------------- + * A message submitted via the contact form on my website. + */ +@Injectable() +export class ContactSubmission extends Model { + protected static table = 'contact_submissions' + protected static key = 'contact_submission_id' + + @Field(FieldType.serial, 'contact_submission_id') + protected id?: number + + @Field(FieldType.varchar) + public email!: string + + @Field(FieldType.varchar) + public name!: string + + @Field(FieldType.text) + public message!: string + + @Field(FieldType.timestamp, 'sent_at') + public sentAt = new Date() +} diff --git a/src/app/resources/views/welcome.pug b/src/app/resources/views/welcome.pug index 3cd717a..e7fb22c 100644 --- a/src/app/resources/views/welcome.pug +++ b/src/app/resources/views/welcome.pug @@ -40,13 +40,11 @@ block content p I'd love to hear from you if you have questions or inquiries related to me or my projects. You can get in touch by text, e-mail, or using this form. I also occasionally share thoughts on my blog. p E-mail: shout@garrettmills.dev .form - form#contact-form + form#contact-form(method='post' action=named('contact')) .form-group input#contactEmail.form-control(type='email' name='email' placeholder='E-Mail Address' required) .form-group - input#contactFirst.form-control(name='first' placeholder='First Name' required) - .form-group - input#contactLast.form-control(name='last' placeholder='Last Name' required) + input#contactFirst.form-control(name='name' placeholder='Name' required) .form-group textarea.form-control#contactMessage(name='message' placeholder='Message' required rows=6) .form-group diff --git a/src/app/types/ContactForm.type.ts b/src/app/types/ContactForm.type.ts new file mode 100644 index 0000000..6076a2f --- /dev/null +++ b/src/app/types/ContactForm.type.ts @@ -0,0 +1,12 @@ + +/** A contact form submission. */ +export interface ContactForm { + /** @email */ + email: string + + /** The submitter's name */ + name: string + + /** The body of the contact form submission. */ + message: string +}