diff --git a/.dockerignore b/.dockerignore index 552203f..a092701 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ ** !src +!types !package.json !package-lock.json !tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f0648d..2284ace 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,14 +38,15 @@ jobs: # TODO: Have working unit tests generated # - name: Run Unit Tests # run: | - # npm run unit - - # TODO: Integration tests requires metacall/FaaS or dashboard.metacall.io credentials - # TODO: unComment when we are ready for integration tests - # - name: Run Tests - # run: | # npm run test + - name: Run Integration Tests + shell: bash + run: | + docker compose build + docker compose up test + docker compose down + - name: Publish uses: JS-DevTools/npm-publish@v1 if: startsWith(github.ref, 'refs/tags/') diff --git a/.gitignore b/.gitignore index be5b109..f383308 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules dist *.env -/logs/app.log \ No newline at end of file +logs diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ad30bfe --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*.yml \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4e0b321 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/dist/server.js", + "preLaunchTask": "npm: buildDebug", + "outFiles": ["${workspaceFolder}/dist/**/*.js"] + } + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..278214e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# +# MetaCall FaaS Script by Parra Studios +# Reimplementation of MetaCall FaaS platform written in TypeScript. +# +# Copyright (C) 2016 - 2024 Vicente Eduardo Ferrer Garcia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +FROM node:20-bookworm-slim AS base + +# Image descriptor +LABEL copyright.name="Vicente Eduardo Ferrer Garcia" \ + copyright.address="vic798@gmail.com" \ + maintainer.name="Vicente Eduardo Ferrer Garcia" \ + maintainer.address="vic798@gmail.com" \ + vendor="MetaCall Inc." \ + version="0.1" + +WORKDIR /metacall + +FROM base AS deps + +COPY . . + +RUN npm install \ + && npm run build + +FROM base as faas + +RUN apt-get update \ + && apt-get install wget ca-certificates -y --no-install-recommends \ + && wget -O - https://raw.githubusercontent.com/metacall/install/master/install.sh | sh + +COPY --from=deps /metacall/node_modules node_modules +COPY --from=deps /metacall/dist dist + +EXPOSE 9000 + +CMD ["node", "dist/index.js"] + +FROM base AS test + +RUN apt-get update \ + && apt-get install curl ca-certificates jq -y --no-install-recommends \ + && npm install -g @metacall/deploy diff --git a/README.md b/README.md index 129b79a..d2f2704 100644 --- a/README.md +++ b/README.md @@ -2,45 +2,56 @@ Reimplementation of MetaCall FaaS platform written in TypeScript. This project requires MetaCall installed in order to run it. For more information about installation: https://github.com/metacall/install -### Development: +### Development + ```sh npm install npm start ``` -### About Project : +### About Project -MetaCall organization has its own cloud platform known as [MetaCall Hub](https://metacall.io/), a production-ready and high-performance FaaS/Cloud platform where you can deploy services, web apps, and lambdas in seconds. However, the ```Real``` version of MetaCall FaaS is commercialized and requires a plan to deploy your polyglot applications, which can be found [Here](https://metacall.io/pricing/). +MetaCall organization has its own cloud platform known as [MetaCall Hub](https://metacall.io/), a production-ready and high-performance FaaS/Cloud platform where you can deploy services, web apps, and lambdas in seconds. However, the `Real` version of MetaCall FaaS is commercialized and requires a plan to deploy your polyglot applications, which can be found [Here](https://metacall.io/pricing/). -When referring to the ```Real``` version of MetaCall FaaS, it should be noted that this refers to the commercialized FaaS cloud service, whereas ```Local``` refers to the mimic version. +When referring to the `Real` version of MetaCall FaaS, it should be noted that this refers to the commercialized FaaS cloud service, whereas `Local` refers to the mimic version. -Soon, we realized that many contributors joining the community needed an paid account on the ```Real FaaS``` for testing their polyglot applications. To remove this barrier, we proposed a project that would mimic the ```Real FaaS```. +Soon, we realized that many contributors joining the community needed an paid account on the `Real FaaS` for testing their polyglot applications. To remove this barrier, we proposed a project that would mimic the `Real FaaS`. With this project, developers can now use it to deploy and test their polyglot applications (built using [MetaCall Core](https://github.com/metacall/core)), web apps, and lambdas. The process is simple: -- Step 1 : Spin up the "Local FaaS" by running the following command: +- Step 1 : Spin up the "Local FaaS" by running the following command: ```sh cd faas npm start ``` -- Step 2 : Install the [metacall-deploy](https://www.npmjs.com/package/@metacall/deploy) NPM package, and wire the ```--dev``` flag with the ```metacall-deploy``` command in your application directory using the following command: +- Step 2 : Install the [metacall-deploy](https://www.npmjs.com/package/@metacall/deploy) NPM package, and wire the `--dev` flag with the `metacall-deploy` command in your application directory using the following command: ```sh cd move-to-application-directory metacall-deploy --dev ``` -### Things that need to be implemented: +### Things that need to be implemented -- In order to mimic the "Real FaaS", we need to create all the API endpoints that the "Real FaaS" supports, which can be found listed [Here](https://github.com/metacall/protocol/blob/master/src/protocol.ts). +- In order to mimic the "Real FaaS", we need to create all the API endpoints that the "Real FaaS" supports, which can be found listed [Here](https://github.com/metacall/protocol/blob/master/src/protocol.ts). ### Important Note -- This project is still under development and there is one extra thing you need to install before running this project and its [MetaCall Core](https://github.com/metacall/core/blob/develop/docs/README.md#41-installation). +- This project is still under development and there is one extra thing you need to install before running this project and its [MetaCall Core](https://github.com/metacall/core/blob/develop/docs/README.md#41-installation). + +- This project is developed using [MetaCall Core] itself in order to provide polyglot support, we are using its [Node Port](https://github.com/metacall/core/tree/develop/source/ports/node_port) of this library to use all the functions and methods `MetaCall Core C API` provides. + +- Also, [Here](https://github.com/metacall/faas/blob/master/types/metacall.d.ts) are all the functions of `MetaCall Core` we are using. -- This project is developed using [MetaCall Core] itself in order to provide polyglot support, we are using its [Node Port](https://github.com/metacall/core/tree/develop/source/ports/node_port) of this library to use all the functions and methods ```MetaCall Core C API``` provides. +### Testing -- Also, [Here](https://github.com/metacall/faas/blob/master/types/metacall.d.ts) are all the functions of ```MetaCall Core``` we are using. +This will run a test, and if the `docker compose up test` command exits with exit code 0, it will mean that the test has passed. +If you want to see the output of the FaaS, run `docker compose up` instead. But when running this, the command won't terminate once the test finishes and the FaaS will keep running. You will need to press `Ctrl+C` in order to close it. +```sh +docker compose build +docker compose up test +docker compose down +``` diff --git a/TODO.md b/TODO.md deleted file mode 100644 index c2c9a09..0000000 --- a/TODO.md +++ /dev/null @@ -1 +0,0 @@ -- We are including the whole metacall-deploy as dependency even that we only use just the ./lib folder, we should abstract this and separate them into different repos, so we dont add unnecesary dependencies to this project. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..234be9d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +# +# MetaCall FaaS Script by Parra Studios +# Reimplementation of MetaCall FaaS platform written in TypeScript. +# +# Copyright (C) 2016 - 2024 Vicente Eduardo Ferrer Garcia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +version: '3.7' + +services: + faas: + image: metacall/faas + container_name: metacall_faas + build: + context: . + dockerfile: Dockerfile + target: faas + ports: + - "9000:9000" + + test: + image: metacall/faas:test + container_name: metacall_faas_test + build: + context: . + dockerfile: Dockerfile + target: test + network_mode: host + depends_on: + - faas + volumes: + - ./test/:/metacall/ + command: /metacall/test.sh diff --git a/package-lock.json b/package-lock.json index bc8a605..65c5aab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,12 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { - "@metacall/protocol": "^0.1.19", + "@metacall/protocol": "^0.1.26", "busboy": "^1.4.0", "colors": "^1.4.0", "dotenv": "^16.0.3", "express": "^4.18.2", "git-clone": "^0.2.0", - "p-queue-es5": "^6.0.2", "unzipper": "^0.10.11" }, "devDependencies": { @@ -24,6 +23,7 @@ "@types/cross-spawn": "^6.0.6", "@types/express": "^4.17.15", "@types/git-clone": "^0.2.0", + "@types/node": "^14.14.7", "@types/mocha": "^10.0.6", "@types/unzipper": "^0.10.5", "@typescript-eslint/eslint-plugin": "^4.7.0", @@ -621,13 +621,11 @@ } }, "node_modules/@metacall/protocol": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@metacall/protocol/-/protocol-0.1.25.tgz", - "integrity": "sha512-Maf9qAry1apWN91kXwiOrttrdkhnXLu/6pxLn2s2mFQKPcK4EI5W1kl5zRvJ/tMd0vEhHAVNzex+ZxoqKAFI6Q==", + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@metacall/protocol/-/protocol-0.1.26.tgz", + "integrity": "sha512-VSP7bCBi9HKXMs1U0k8ztgxAeRKUFd/71Wu51PjCEk+9wbUupioev0cuqIlpkCzxi2gnQRdNFpBkfx8cRwVE7g==", "hasInstallScript": true, "dependencies": { - "@types/ignore-walk": "^4.0.0", - "@types/jsonwebtoken": "^8.5.8", "axios": "^0.21.0", "form-data": "^3.0.0", "ignore-walk": "^3.0.4", @@ -762,28 +760,12 @@ "integrity": "sha512-RnYrs8sZWTBfeg8hAh5VwM3wEBlIaraQfo8h9FSxmX09KQ9VUDuL9AFzR3doV6IUMzEwjee6fOViIhqFvs1ulA==", "dev": true }, - "node_modules/@types/ignore-walk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/ignore-walk/-/ignore-walk-4.0.0.tgz", - "integrity": "sha512-T5+1xZfSpgIz7FntMveRpKdLWZhPOidwaxVjUjGjyCXsaD2cVFr4a1+qd2HPcsM9qP0XBkg4jC9NdmuD3lZeqQ==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, - "node_modules/@types/jsonwebtoken": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", - "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -797,9 +779,10 @@ "dev": true }, "node_modules/@types/node": { - "version": "15.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", - "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==" + "version": "14.14.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz", + "integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==", + "dev": true }, "node_modules/@types/qs": { "version": "6.9.7", @@ -1197,12 +1180,12 @@ "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -1210,7 +1193,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -1353,12 +1336,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1619,9 +1608,9 @@ } }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "engines": { "node": ">= 0.6" } @@ -1633,9 +1622,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -1699,21 +1688,6 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "node_modules/default-require-extensions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", - "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", - "dev": true, - "dependencies": { - "strip-bom": "^4.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1833,12 +1807,6 @@ "node": ">=8.6" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2144,22 +2112,17 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" - }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2369,9 +2332,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -2494,9 +2457,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -2523,13 +2489,18 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2624,6 +2595,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -2633,6 +2615,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -2649,40 +2632,37 @@ "node": ">=4" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasha": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", - "dev": true, - "dependencies": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasha/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/he": { @@ -3122,14 +3102,20 @@ } }, "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "dependencies": { "jws": "^3.2.2", - "lodash": "^4.17.21", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", "ms": "^2.1.1", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">=12", @@ -3191,7 +3177,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.clonedeep": { "version": "4.5.0", @@ -3199,18 +3186,17 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, - "node_modules/lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -3755,9 +3741,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3798,14 +3784,6 @@ "node": ">= 0.8.0" } }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "engines": { - "node": ">=4" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3836,18 +3814,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-queue-es5": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/p-queue-es5/-/p-queue-es5-6.0.2.tgz", @@ -3871,30 +3837,6 @@ "node": ">=8" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/package-hash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", - "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4175,9 +4117,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -4439,12 +4381,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -4477,13 +4413,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5584,12 +5524,10 @@ } }, "@metacall/protocol": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@metacall/protocol/-/protocol-0.1.25.tgz", - "integrity": "sha512-Maf9qAry1apWN91kXwiOrttrdkhnXLu/6pxLn2s2mFQKPcK4EI5W1kl5zRvJ/tMd0vEhHAVNzex+ZxoqKAFI6Q==", + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@metacall/protocol/-/protocol-0.1.26.tgz", + "integrity": "sha512-VSP7bCBi9HKXMs1U0k8ztgxAeRKUFd/71Wu51PjCEk+9wbUupioev0cuqIlpkCzxi2gnQRdNFpBkfx8cRwVE7g==", "requires": { - "@types/ignore-walk": "^4.0.0", - "@types/jsonwebtoken": "^8.5.8", "axios": "^0.21.0", "form-data": "^3.0.0", "ignore-walk": "^3.0.4", @@ -5715,28 +5653,12 @@ "integrity": "sha512-RnYrs8sZWTBfeg8hAh5VwM3wEBlIaraQfo8h9FSxmX09KQ9VUDuL9AFzR3doV6IUMzEwjee6fOViIhqFvs1ulA==", "dev": true }, - "@types/ignore-walk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/ignore-walk/-/ignore-walk-4.0.0.tgz", - "integrity": "sha512-T5+1xZfSpgIz7FntMveRpKdLWZhPOidwaxVjUjGjyCXsaD2cVFr4a1+qd2HPcsM9qP0XBkg4jC9NdmuD3lZeqQ==", - "requires": { - "@types/node": "*" - } - }, "@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, - "@types/jsonwebtoken": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", - "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", - "requires": { - "@types/node": "*" - } - }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -5750,9 +5672,10 @@ "dev": true }, "@types/node": { - "version": "15.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", - "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==" + "version": "14.14.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz", + "integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==", + "dev": true }, "@types/qs": { "version": "6.9.7", @@ -6028,12 +5951,12 @@ "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -6041,7 +5964,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -6144,12 +6067,15 @@ } }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "callsites": { @@ -6335,9 +6261,9 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "convert-source-map": { "version": "1.9.0", @@ -6346,9 +6272,9 @@ "dev": true }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" }, "cookie-signature": { "version": "1.0.6", @@ -6392,15 +6318,6 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "default-require-extensions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", - "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", - "dev": true, - "requires": { - "strip-bom": "^4.0.0" - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6492,12 +6409,6 @@ "ansi-colors": "^4.1.1" } }, - "es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true - }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -6716,22 +6627,17 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, - "eventemitter3": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" - }, "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -6912,9 +6818,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "foreground-child": { "version": "2.0.0", @@ -6986,9 +6892,9 @@ } }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -7009,13 +6915,15 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "get-package-type": { @@ -7080,6 +6988,14 @@ "slash": "^3.0.0" } }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -7089,6 +7005,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -7099,29 +7016,24 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, - "hasha": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", - "dev": true, - "requires": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -7449,14 +7361,20 @@ "dev": true }, "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "requires": { "jws": "^3.2.2", - "lodash": "^4.17.21", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", "ms": "^2.1.1", - "semver": "^7.3.8" + "semver": "^7.5.4" } }, "jwa": { @@ -7505,7 +7423,8 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "lodash.clonedeep": { "version": "4.5.0", @@ -7513,18 +7432,17 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", - "dev": true - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -7938,9 +7856,9 @@ } }, "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" }, "on-finished": { "version": "2.4.1", @@ -7972,11 +7890,6 @@ "word-wrap": "^1.2.3" } }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7995,15 +7908,6 @@ "p-limit": "^3.0.2" } }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, "p-queue-es5": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/p-queue-es5/-/p-queue-es5-6.0.2.tgz", @@ -8021,24 +7925,6 @@ "p-finally": "^1.0.0" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "package-hash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", - "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - } - }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8232,9 +8118,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -8425,12 +8311,6 @@ "send": "0.18.0" } }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -8457,13 +8337,14 @@ "dev": true }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { diff --git a/package.json b/package.json index 68a521d..984089f 100644 --- a/package.json +++ b/package.json @@ -2,16 +2,15 @@ "name": "@metacall/faas", "version": "0.1.0", "description": "Reimplementation of MetaCall FaaS platform written in TypeScript.", - "main": "dist/server.js", + "main": "dist/index.js", "scripts": { - "test": "npm run --silent build && mocha dist/test", - "coverage": "nyc npm run test", - "unit": "npm run --silent test -- --ignore **/integration**", - "prepublishOnly": "npm run --silent build", - "build": "npm run --silent lint && tsc", + "test": "npm run build && mocha dist/test", + "prepublishOnly": "npm run build", + "buildDebug": "npm run lint && tsc --sourceMap true", + "build": "npm run lint && tsc", "lint": "eslint . --max-warnings=0 --ignore-pattern dist", "fix": "eslint . --max-warnings=0 --ignore-pattern dist --fix", - "start": "npm run build && metacall dist/server.js" + "start": "npm run build && node dist/index.js" }, "repository": { "type": "git", @@ -64,17 +63,34 @@ "project": "./tsconfig.json" }, "rules": { - "tsdoc/syntax": "warn" + "tsdoc/syntax": "warn", + "no-unused-vars": "off", + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "property", + "format": [ + "camelCase" + ] + } + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ] } }, "dependencies": { - "@metacall/protocol": "^0.1.19", + "@metacall/protocol": "^0.1.26", "busboy": "^1.4.0", "colors": "^1.4.0", "dotenv": "^16.0.3", "express": "^4.18.2", "git-clone": "^0.2.0", - "p-queue-es5": "^6.0.2", "unzipper": "^0.10.11" }, "devDependencies": { @@ -84,6 +100,7 @@ "@types/express": "^4.17.15", "@types/git-clone": "^0.2.0", "@types/mocha": "^10.0.6", + "@types/node": "^14.14.7", "@types/unzipper": "^0.10.5", "@typescript-eslint/eslint-plugin": "^4.7.0", "@typescript-eslint/parser": "^4.7.0", @@ -98,4 +115,4 @@ "prettier": "^2.1.2", "typescript": "^4.3.2" } -} +} \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index 42bc30e..b347367 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,299 +1,27 @@ -import { spawn } from 'child_process'; -import colors from 'colors'; -import { NextFunction, Request, Response } from 'express'; -import { hostname } from 'os'; -import * as path from 'path'; - -import deployDeleteController from './controller/delete'; -import uploadController from './controller/upload'; - -import { - allApplications, - childProcessResponse, - cps, - currentFile, - deleteBody, - deployBody, - fetchBranchListBody, - fetchFilesFromRepoBody, - protocol -} from './constants'; - -import AppError from './utils/appError'; +import { callFunction } from './controller/call'; +import { deployDelete } from './controller/delete'; +import { deploy } from './controller/deploy'; +import { globalError } from './controller/error'; +import { logs } from './controller/logs'; import { - calculatePackages, - catchAsync, - deleteRepoFolderIfExist, - dirName, - ensureFolderExists, - execPromise, - exists, - installDependencies, - isIAllApps, - logProcessOutput -} from './utils/utils'; - -import { PackageError } from '@metacall/protocol/package'; -import { appsDirectory } from './utils/config'; - -const appsDir = appsDirectory(); - -colors.enable(); - -export const callFnByName = ( - req: Request, - res: Response, - next: NextFunction -): Response | void => { - if (!(req.params && req.params.name)) - next( - new AppError( - 'A function name is required in the path; i.e: /call/sum.', - 404 - ) - ); - - const { appName: app, name } = req.params; - const args = Object.values(req.body); - - if (!(app in cps)) { - return res - .status(404) - .send( - `Oops! It looks like the application (${app}) hasn't been deployed yet. Please deploy it before you can call its functions.` - ); - } - - let responseSent = false; // Flag to track if response has been sent - let errorCame = false; - - cps[app].send({ - type: protocol.c, - fn: { - name, - args - } - }); - - cps[app].on('message', (data: childProcessResponse) => { - if (!responseSent) { - // Check if response has already been sent - if (data.type === protocol.r) { - responseSent = true; // Set flag to true to indicate response has been sent - return res.send(JSON.stringify(data.data)); - } else { - errorCame = true; - } - } - }); - - // Default response in case the 'message' event is not triggered - if (!responseSent && errorCame) { - responseSent = true; // Set flag to true to indicate response has been sent - errorCame = false; - return res.send('Function calling error'); - } + fetchBranchList, + fetchFileList, + fetchFilesFromRepo +} from './controller/repository'; +import { serveStatic } from './controller/static'; +import { uploadPackage } from './controller/upload'; +import { validate } from './controller/validate'; + +export default { + callFunction, + deployDelete, + deploy, + globalError, + logs, + fetchFilesFromRepo, + fetchBranchList, + fetchFileList, + serveStatic, + uploadPackage, + validate }; - -export const serveStatic = catchAsync( - async (req: Request, res: Response, next: NextFunction) => { - if (!req.params) next(new AppError('Invalid API endpoint', 404)); - - const { app, file } = req.params; - - const appLocation = path.join(appsDir, `${app}/${file}`); - - // TODO - The best way to handle this is first list all the application which has been deployed and match if there is application or not and then go for file search - - if (!(await exists(appLocation))) - next( - new AppError( - "The file you're looking for might not be available or the application may not be deployed.", - 404 - ) - ); - - res.status(200).sendFile(appLocation); - } -); - -export const fetchFiles = ( - req: Request, - res: Response, - next: NextFunction -): void => uploadController(req, res, next); - -export const fetchFilesFromRepo = catchAsync( - async ( - req: Omit & { body: fetchFilesFromRepoBody }, - res: Response, - next: NextFunction - ) => { - const { branch, url } = req.body; - - await ensureFolderExists(appsDir); - - try { - deleteRepoFolderIfExist(appsDir, url); - } catch (err) { - next( - new AppError( - 'error occurred in deleting repository directory', - 500 - ) - ); - } - - await execPromise( - `cd ${appsDir}; git clone --single-branch --depth=1 --branch ${branch} ${url} ` - ); - - const id = dirName(req.body.url); - - currentFile['id'] = id; - currentFile.path = `${appsDir}/${id}`; - - res.status(201).send({ id }); - } -); - -export const fetchBranchList = catchAsync( - async ( - req: Omit & { body: fetchBranchListBody }, - res: Response - ) => { - const { stdout } = await execPromise( - `git ls-remote --heads ${req.body.url}` - ); - - const branches: string[] = []; - - JSON.stringify(stdout.toString()) - .split('\\n') - .forEach(el => { - if (el.trim().length > 1) { - branches.push(el.split('refs/heads/')[1]); - } - }); - - res.send({ branches }); - } -); - -export const fetchFileList = catchAsync( - async ( - req: Omit & { body: fetchFilesFromRepoBody }, - res: Response, - next: NextFunction - ) => { - await ensureFolderExists(appsDir); - - try { - deleteRepoFolderIfExist(appsDir, req.body.url); - } catch (err) { - next( - new AppError( - 'error occurred in deleting repository directory', - 500 - ) - ); - } - await execPromise( - `cd ${appsDir} ; git clone ${req.body.url} --depth=1 --no-checkout` - ); - - const dirPath = `${appsDir}/${dirName(req.body.url)}`; - - const { stdout } = await execPromise( - `cd ${dirPath} ; git ls-tree -r ${req.body.branch} --name-only; cd .. ; rm -r ${dirPath}` - ); - - res.send({ files: JSON.stringify(stdout.toString()).split('\\n') }); - } -); - -export const deploy = catchAsync( - async ( - req: Omit & { body: deployBody }, - res: Response, - next: NextFunction - ) => { - try { - req.body.resourceType == 'Repository' && - (await calculatePackages(next)); - - // TODO Currently Deploy function will only work for workdir, we will add the addRepo - - await installDependencies(); - - const desiredPath = path.join(__dirname, '/worker/index.js'); - - const proc = spawn('metacall', [desiredPath], { - stdio: ['pipe', 'pipe', 'pipe', 'ipc'] - }); - - proc.send({ - type: protocol.l, - currentFile - }); - - logProcessOutput(proc.stdout, 'green'); - logProcessOutput(proc.stderr, 'red'); - - proc.on('message', (data: childProcessResponse) => { - if (data.type === protocol.g) { - if (isIAllApps(data.data)) { - const appName = Object.keys(data.data)[0]; - cps[appName] = proc; - allApplications[appName] = data.data[appName]; - } - } - }); - - res.status(200).json({ - suffix: hostname(), - prefix: currentFile.id, - version: 'v1' - }); - } catch (err) { - // Check if the error is PackageError.Empty - if (err === PackageError.Empty) { - return next(err); - } - return next(err); - } - } -); - -export const showLogs = (req: Request, res: Response): Response => { - return res.send('Demo Logs...'); -}; - -export const deployDelete = ( - req: Omit & { body: deleteBody }, - res: Response -): Response => deployDeleteController(req, res); - -export const validateAndDeployEnabled = ( - req: Request, - res: Response -): Response => - res.status(200).json({ - status: 'success', - data: true - }); - -/** - * deploy - * Provide a mesage that repo has been deployed, use --inspect to know more about deployment - * We can add the type of url in the --inspect - * If there is already metacall.json present then, log found metacall.json and reading it, reading done - * We must an option to go back in the fileselection wizard so that, user dont have to close the connection - * At the end of deployment through deploy cli, we should run the --inspect command so that current deployed file is shown and show only the current deployed app - * - * - * FAAS - * the apps are not getting detected once the server closes, do we need to again deploy them - * find a way to detect metacall.json in the files and dont deploy if there is not because json ke through we are uploading - * - */ diff --git a/src/app.ts b/src/app.ts index a81956c..3308d96 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,12 +2,9 @@ import { hostname } from 'os'; import express, { NextFunction, Request, Response } from 'express'; -import * as api from './api'; +import api from './api'; import { allApplications } from './constants'; import AppError from './utils/appError'; -import { findJsonFilesRecursively } from './utils/autoDeploy'; -import { appsDirectory } from './utils/config'; -import globalErrorHandler from './utils/errorHandler'; const app = express(); const host = hostname(); @@ -15,25 +12,27 @@ const host = hostname(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); -app.get('/validate', api.validateAndDeployEnabled); -app.get('/api/account/deploy-enabled', api.validateAndDeployEnabled); +app.get('/readiness', (_req: Request, res: Response) => res.sendStatus(200)); +app.get('/validate', api.validate); +app.get('/api/account/deploy-enabled', api.validate); -app.post(`/${host}/:appName/:version/call/:name`, api.callFnByName); +app.get(`/${host}/:appName/:version/call/:name`, api.callFunction); +app.post(`/${host}/:appName/:version/call/:name`, api.callFunction); app.get( `/${host}/:appName/:version/static/.metacall/faas/apps/:app/:file`, api.serveStatic ); -app.post('/api/package/create', api.fetchFiles); +app.post('/api/package/create', api.uploadPackage); app.post('/api/repository/add', api.fetchFilesFromRepo); app.post('/api/repository/branchlist', api.fetchBranchList); app.post('/api/repository/filelist', api.fetchFileList); -app.post('/api/deploy/logs', api.showLogs); +app.post('/api/deploy/logs', api.logs); app.post('/api/deploy/create', api.deploy); -app.get('/api/inspect', (req, res) => { +app.get('/api/inspect', (_req, res) => { res.send(Object.values(allApplications)); }); @@ -44,15 +43,6 @@ app.all('*', (req: Request, res: Response, next: NextFunction) => { next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404)); }); -const appsDir = appsDirectory(); -findJsonFilesRecursively(appsDir) - .then(() => { - console.log('Previously deployed apllications deployed successfully'); - }) - .catch(error => { - console.error('Error while re-deploying applications', error); - }); - -app.use(globalErrorHandler); +app.use(api.globalError); export default app; diff --git a/src/constants.ts b/src/constants.ts index 8ddc58b..5755f12 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,21 +1,16 @@ import { DeployStatus, MetaCallJSON } from '@metacall/protocol/deployment'; import { ChildProcess } from 'child_process'; -export interface currentUploadedFile { +export interface Deployment { id: string; type?: string; jsons: MetaCallJSON[]; runners?: string[]; path: string; + blob?: string; } -export const currentFile: currentUploadedFile = { - id: '', - type: '', - jsons: [], - runners: [], - path: '' -}; +export const deploymentMap: Record> = {}; export const createInstallDependenciesScript = ( runner: string, @@ -30,34 +25,9 @@ export const createInstallDependenciesScript = ( return installDependenciesScript[runner]; }; -export type namearg = 'id' | 'type' | 'jsons' | 'runners' | 'path'; -export type valueArg = string; - -export type fetchFilesFromRepoBody = { - branch: 'string'; - url: 'string'; -}; -export type fetchBranchListBody = { - url: 'string'; -}; - -export type deployBody = { - suffix: string; //name of deployment - resourceType: 'Package' | 'Repository'; - release: string; //release date - env: string[]; - plan: string; - version: string; -}; - -export type deleteBody = { - suffix: string; //name of deployment - prefix: string; - version: string; -}; - export type tpackages = Record; +// TODO: Isn't this available inside protocol package? We MUST reuse it export interface IApp { status: DeployStatus; prefix: string; @@ -67,6 +37,7 @@ export interface IApp { ports: number[]; } +// TODO: Isn't this available inside protocol package? We MUST reuse it export class App implements IApp { public status: DeployStatus; public prefix: string; @@ -100,21 +71,46 @@ export type IAllApps = Record; export const allApplications: IAllApps = {}; -export const protocol = { - i: 'installDependencies', - l: 'loadFunctions', - g: 'getApplicationMetadata', - c: 'callFunction', - r: 'functionInvokeResult' -}; +export enum ProtocolMessageType { + Install = 'InstallDependencies', + Load = 'LoadFunctions', + MetaData = 'GetApplicationMetadata', + Invoke = 'CallFunction', + InvokeResult = 'FunctionInvokeResult' +} -export const cps: { [key: string]: ChildProcess } = {}; +export const childProcesses: { [key: string]: ChildProcess } = {}; -export interface childProcessResponse { - type: keyof typeof protocol; - data: unknown; +export interface WorkerMessage { + type: ProtocolMessageType; + data: T; } +export type WorkerMessageUnknown = WorkerMessage; + export interface InspectObject { [key: string]: Array<{ name: string }>; } +export interface LogMessage { + deploymentName: string; + workerPID: number; + message: string; +} + +export const ANSICode: number[] = [ + 166, 154, 142, 118, 203, 202, 190, 215, 214, 32, 6, 4, 220, 208, 184, 172 +]; + +export interface PIDToColorCodeMapType { + [key: string]: number; +} + +export interface AssignedColorCodesType { + [key: string]: boolean; +} + +// Maps a PID to a color code +export const PIDToColorCodeMap: PIDToColorCodeMapType = {}; + +// Tracks whether a color code is assigned +export const assignedColorCodes: AssignedColorCodesType = {}; diff --git a/src/controller/call.ts b/src/controller/call.ts new file mode 100644 index 0000000..fa80bdc --- /dev/null +++ b/src/controller/call.ts @@ -0,0 +1,62 @@ +import { NextFunction, Request, Response } from 'express'; +import { + ProtocolMessageType, + WorkerMessageUnknown, + childProcesses +} from '../constants'; +import AppError from '../utils/appError'; + +export const callFunction = ( + req: Request, + res: Response, + next: NextFunction +): Response | void => { + if (!(req.params && req.params.name)) + next( + new AppError( + 'A function name is required in the path; i.e: /call/sum.', + 404 + ) + ); + + const { appName: app, name } = req.params; + const args = Object.values(req.body); + + if (!(app in childProcesses)) { + return res + .status(404) + .send( + `Oops! It looks like the application (${app}) hasn't been deployed yet. Please deploy it before you can call its functions.` + ); + } + + let responseSent = false; // Flag to track if response has been sent + let errorCame = false; + + childProcesses[app].send({ + type: ProtocolMessageType.Invoke, + data: { + name, + args + } + }); + + childProcesses[app].on('message', (message: WorkerMessageUnknown) => { + if (!responseSent) { + // Check if response has already been sent + if (message.type === ProtocolMessageType.InvokeResult) { + responseSent = true; // Set flag to true to indicate response has been sent + return res.send(JSON.stringify(message.data)); + } else { + errorCame = true; + } + } + }); + + // Default response in case the 'message' event is not triggered + if (!responseSent && errorCame) { + responseSent = true; // Set flag to true to indicate response has been sent + errorCame = false; + return res.send('Function calling error'); + } +}; diff --git a/src/controller/delete.ts b/src/controller/delete.ts index 1ff4426..05c4296 100644 --- a/src/controller/delete.ts +++ b/src/controller/delete.ts @@ -1,46 +1,82 @@ +import { NextFunction, Request, Response } from 'express'; + import { ChildProcess } from 'child_process'; import { rmSync } from 'fs'; import { join } from 'path'; -import { Request, Response } from 'express'; - -import { allApplications, cps, deleteBody } from '../constants'; +import { allApplications, childProcesses } from '../constants'; import { appsDirectory } from '../utils/config'; -import { deleteStatusMessage } from '../utils/resposeTexts'; +import { catchAsync, ensureFolderExists } from '../utils/utils'; -export default ( - req: Omit & { body: deleteBody }, - res: Response -): Response => { - // Extract the suffix (application name) of the application from the request body - const { suffix: app } = req.body; +const deleteStatusMessage = ( + app: string +): { + success: string; + error: string; + folderShouldntExist: string; + appShouldntExist: string; +} => ({ + success: 'Deploy Delete Succeed', + error: `Oops! It looks like the application (${app}) hasn't been deployed yet. Please deploy it before you delete it.`, + folderShouldntExist: `The folder shouldnt exist after deleting it.`, + appShouldntExist: `The application shouldnt exist after deleting it` +}); - // Initialize isError flag - let isError = false; +// TODO: Isn't this available inside protocol package? We MUST reuse it +type DeleteBody = { + suffix: string; // name of deployment + prefix: string; + version: string; +}; - // Check if the application exists in cps and allApplications objects - if (!(app in cps && app in allApplications)) { - isError = true; - } +// TODO: Refactor this, do not use sync methods +export const deployDelete = catchAsync( + async ( + req: Omit & { body: DeleteBody }, + res: Response, + _next: NextFunction + ): Promise => { + // Extract the suffix (application name) of the application from the request body + const { suffix: app } = req.body; - // Retrieve the child process associated with the application and kill it - const appCP: ChildProcess = cps[app]; - appCP.kill(); + // Initialize isError flag + let isError = false; - // Remove the application from cps and allApplications objects - delete cps[app]; - delete allApplications[app]; + // Check if the application exists in childProcesses and allApplications objects + if (!(app in childProcesses && app in allApplications)) { + isError = true; + return res.send(deleteStatusMessage(app)['error']); + } - // Determine the location of the application - const appLocation = join(appsDirectory(), app); + // Retrieve the child process associated with the application and kill it + const childProcessesInApplications: ChildProcess = childProcesses[app]; + childProcessesInApplications.kill(); - // Delete the directory of the application - rmSync(appLocation, { recursive: true, force: true }); + // Remove the application from childProcesses and allApplications objects + delete childProcesses[app]; + delete allApplications[app]; - // Send response based on whether there was an error during deletion - return res.send( - isError - ? deleteStatusMessage(app)['error'] - : deleteStatusMessage(app)['success'] - ); -}; + if (app in childProcesses && app in allApplications) { + isError = true; + return res.send(deleteStatusMessage(app)['appShouldntExist']); + } + + // Determine the location of the application + const appLocation = join(appsDirectory, app); + + // Delete the directory of the application + rmSync(appLocation, { recursive: true, force: true }); + + if (!(await ensureFolderExists(appLocation))) { + isError = true; + return res.send(deleteStatusMessage(app)['folderShouldntExist']); + } + + // Send response based on whether there was an error during deletion + return res.send( + isError + ? deleteStatusMessage(app)['error'] + : deleteStatusMessage(app)['success'] + ); + } +); diff --git a/src/controller/deploy.ts b/src/controller/deploy.ts new file mode 100644 index 0000000..3199647 --- /dev/null +++ b/src/controller/deploy.ts @@ -0,0 +1,95 @@ +import { spawn } from 'child_process'; +import { NextFunction, Request, Response } from 'express'; +import { hostname } from 'os'; +import path from 'path'; + +import { + ProtocolMessageType, + WorkerMessageUnknown, + allApplications, + childProcesses, + deploymentMap +} from '../constants'; + +import AppError from '../utils/appError'; + +import { + catchAsync, + installDependencies, + isIAllApps, + logProcessOutput +} from '../utils/utils'; + +// TODO: Isn't this available inside protocol package? We MUST reuse it +export type DeployBody = { + suffix: string; // name of deployment + resourceType: 'Package' | 'Repository'; + release: string; // release date + env: string[]; + plan: string; + version: string; +}; + +export const deploy = catchAsync( + async ( + req: Omit & { body: DeployBody }, + res: Response, + next: NextFunction + ) => { + try { + // TODO: Implement repository + // req.body.resourceType == 'Repository' && + // (await calculatePackages(next)); + + const deployment = await deploymentMap[req.body.suffix]; + + if (deployment === undefined) { + return next( + new AppError( + `Invalid deployment id: ${req.body.suffix}`, + 400 + ) + ); + } + + await installDependencies(deployment); + + const desiredPath = path.join( + path.resolve(__dirname, '..'), + 'worker', + 'index.js' + ); + + const proc = spawn('metacall', [desiredPath], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] + }); + + proc.send({ + type: ProtocolMessageType.Load, + data: deployment + }); + + logProcessOutput(proc.stdout, proc.pid, deployment.id); + logProcessOutput(proc.stderr, proc.pid, deployment.id); + + proc.on('message', (payload: WorkerMessageUnknown) => { + if (payload.type === ProtocolMessageType.MetaData) { + const message = payload; + if (isIAllApps(message.data)) { + const appName = Object.keys(message.data)[0]; + childProcesses[appName] = proc; + allApplications[appName] = message.data[appName]; + } + } + }); + + return res.status(200).json({ + prefix: hostname(), + suffix: deployment.id, + version: 'v1' + }); + } catch (err) { + return next(err); + } + } +); diff --git a/src/utils/errorHandler.ts b/src/controller/error.ts similarity index 74% rename from src/utils/errorHandler.ts rename to src/controller/error.ts index 204869d..7e1d9a4 100644 --- a/src/utils/errorHandler.ts +++ b/src/controller/error.ts @@ -1,13 +1,11 @@ -/* eslint-disable */ - import { NextFunction, Request, Response } from 'express'; -import { IAppError } from './appError'; +import { IAppError } from '../utils/appError'; -const globalErroHandler = ( +export const globalError = ( err: IAppError, req: Request, res: Response, - next: NextFunction + _next: NextFunction ): Response => { err.statusCode = err.statusCode || 500; err.status = err.status || 'error'; @@ -22,5 +20,3 @@ const globalErroHandler = ( return res.status(err.statusCode).send(err.message); }; - -export default globalErroHandler; diff --git a/src/controller/logs.ts b/src/controller/logs.ts new file mode 100644 index 0000000..69f7953 --- /dev/null +++ b/src/controller/logs.ts @@ -0,0 +1,6 @@ +import { Request, Response } from 'express'; + +// TODO: Implement logs +export const logs = (req: Request, res: Response): Response => { + return res.send('TODO: Implement Logs...'); +}; diff --git a/src/controller/repository.ts b/src/controller/repository.ts new file mode 100644 index 0000000..affcb87 --- /dev/null +++ b/src/controller/repository.ts @@ -0,0 +1,118 @@ +import { NextFunction, Request, Response } from 'express'; +import { promises as fs } from 'fs'; +import { join } from 'path'; +import AppError from '../utils/appError'; +import { appsDirectory } from '../utils/config'; +import { catchAsync, execPromise } from '../utils/utils'; + +// TODO: Isn't this available inside protocol package? We MUST reuse it +type FetchFilesFromRepoBody = { + branch: string; + url: string; +}; + +// TODO: Isn't this available inside protocol package? We MUST reuse it +type FetchBranchListBody = { + url: string; +}; + +const dirName = (gitUrl: string): string => + String(gitUrl.split('/')[gitUrl.split('/').length - 1]).replace('.git', ''); + +const deleteRepoFolderIfExist = async ( + path: Path, + url: string +): Promise => { + const folder = dirName(url); + const repoFilePath = join(path, folder); + + await fs.rm(repoFilePath, { recursive: true, force: true }); +}; + +export const fetchFilesFromRepo = catchAsync( + async ( + req: Omit & { body: FetchFilesFromRepoBody }, + res: Response, + next: NextFunction + ) => { + const { branch, url } = req.body; + + try { + await deleteRepoFolderIfExist(appsDirectory, url); + } catch (err) { + next( + new AppError( + 'error occurred in deleting repository directory', + 500 + ) + ); + } + + await execPromise( + `cd ${appsDirectory} && git clone --single-branch --depth=1 --branch ${branch} ${url} ` + ); + + const id = dirName(req.body.url); + + // TODO: This method is wrong + // deployment.id = id; + // deployment.path = `${appsDir}/${id}`; + + return res.status(201).send({ id }); + } +); + +export const fetchBranchList = catchAsync( + async ( + req: Omit & { body: FetchBranchListBody }, + res: Response + ) => { + const { stdout } = await execPromise( + `git ls-remote --heads ${req.body.url}` + ); + + const branches: string[] = []; + + JSON.stringify(stdout.toString()) + .split('\\n') + .forEach(el => { + if (el.trim().length > 1) { + branches.push(el.split('refs/heads/')[1]); + } + }); + + return res.send({ branches }); + } +); + +export const fetchFileList = catchAsync( + async ( + req: Omit & { body: FetchFilesFromRepoBody }, + res: Response, + next: NextFunction + ) => { + try { + await deleteRepoFolderIfExist(appsDirectory, req.body.url); + } catch (err) { + next( + new AppError( + 'error occurred in deleting repository directory', + 500 + ) + ); + } + await execPromise( + `cd ${appsDirectory} ; git clone ${req.body.url} --depth=1 --no-checkout` + ); + + const dirPath = `${appsDirectory}/${dirName(req.body.url)}`; + + const { stdout } = await execPromise( + `cd ${dirPath} ; git ls-tree -r ${req.body.branch} --name-only; cd .. ; rm -r ${dirPath}` + ); + + return res.send({ + files: JSON.stringify(stdout.toString()).split('\\n') + }); + } +); diff --git a/src/controller/static.ts b/src/controller/static.ts new file mode 100644 index 0000000..7b31110 --- /dev/null +++ b/src/controller/static.ts @@ -0,0 +1,36 @@ +import { NextFunction, Request, Response } from 'express'; +import path from 'path'; + +import { childProcesses } from '../constants'; +import AppError from '../utils/appError'; +import { appsDirectory } from '../utils/config'; +import { catchAsync, exists } from '../utils/utils'; + +export const serveStatic = catchAsync( + async (req: Request, res: Response, next: NextFunction): Promise => { + if (!req.params) next(new AppError('Invalid API endpoint', 404)); + + const { app, file } = req.params; + + const appLocation = path.join(appsDirectory, `${app}/${file}`); + + if (!(app in childProcesses)) { + next( + new AppError( + `Oops! It looks like the application (${app}) hasn't been deployed yet. Please deploy it before you can call its functions.`, + 404 + ) + ); + } + + if (!(await exists(appLocation))) + next( + new AppError( + "The file you're looking for might not be available or the application may not be deployed.", + 404 + ) + ); + + return res.status(200).sendFile(appLocation); + } +); diff --git a/src/controller/upload.ts b/src/controller/upload.ts index c142ab1..7bdf4a2 100644 --- a/src/controller/upload.ts +++ b/src/controller/upload.ts @@ -1,102 +1,198 @@ import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import busboy from 'busboy'; import { NextFunction, Request, Response } from 'express'; -import PQueue from 'p-queue-es5'; -import { Extract } from 'unzipper'; +import { Extract, ParseOptions } from 'unzipper'; -import { currentFile, namearg } from '../constants'; +import { Deployment, deploymentMap } from '../constants'; import { MetaCallJSON } from '@metacall/protocol/deployment'; import AppError from '../utils/appError'; import { appsDirectory } from '../utils/config'; +import { ensureFolderExists } from '../utils/utils'; + +const getUploadError = ( + on: keyof busboy.BusboyEvents, + error: Error +): AppError => { + const internalError = () => ({ + message: `Please upload your zip file again, Internal Server Error: ${error.toString()}`, + code: 500 + }); -const appsDir = appsDirectory(); - -const getUploadError = (on: keyof busboy.BusboyEvents): AppError => { - const errorUploadMessage: Record = { - file: 'Error while fetching the zip file, please upload it again', - field: 'You might be sending improperly formed multipart form data fields or jsons', - finish: 'Internal Server Error, Please upload your zip file again' + const errorUploadMessage: Record< + string, + { message: string; code: number } + > = { + file: { + message: + 'Error while fetching the zip file, please upload it again', + code: 400 + }, + field: { + message: + 'You might be sending improperly formed multipart form data fields or jsons', + code: 400 + }, + finish: internalError() }; - const message = - errorUploadMessage[on.toString()] || - 'Internal Server Error, Please upload the zip again'; + const appError = errorUploadMessage[on.toString()] || internalError(); - return new AppError(message, 500); + return new AppError(appError.message, appError.code); }; -export default (req: Request, res: Response, next: NextFunction): void => { +export const uploadPackage = ( + req: Request, + res: Response, + next: NextFunction +): void => { const bb = busboy({ headers: req.headers }); - const queue = new PQueue({ concurrency: 1 }); - - const handleError = (fn: () => void, on: keyof busboy.BusboyEvents) => { - queue - .add(() => { - try { - fn(); - } catch (e) { - req.unpipe(bb); - queue.pause(); - next(getUploadError(on)); - } - }) - .catch(err => { - req.unpipe(bb); - queue.pause(); - next(err); - }); + const deployment: Deployment = { + id: '', + type: '', + path: '', + jsons: [] }; - bb.on('file', (name, file, info) => { - const { mimeType, filename } = info; - if ( - mimeType != 'application/x-zip-compressed' && - mimeType != 'application/zip' - ) - return next(new AppError('Please upload a zip file', 404)); - - handleError(() => { - const saveTo = path.join(__dirname, filename); - currentFile.path = saveTo; - file.pipe(fs.createWriteStream(saveTo)); - }, 'file'); - }); + const errorHandler = (error: AppError) => { + req.unpipe(bb); + next(error); + }; - bb.on('field', (name: namearg, val: string) => { - handleError(() => { - if (name === 'runners') { - currentFile['runners'] = JSON.parse(val) as string[]; - } else if (name === 'jsons') { - currentFile['jsons'] = JSON.parse(val) as MetaCallJSON[]; - } else { - currentFile[name] = val; + const eventHandler = (type: keyof busboy.BusboyEvents, listener: T) => { + bb.on(type, (...args: unknown[]) => { + try { + const fn = listener as unknown as (...args: unknown[]) => void; + fn(...args); + } catch (e) { + errorHandler(getUploadError(type, e as Error)); } - }, 'field'); - }); + }); + }; - bb.on('finish', () => { - handleError(() => { - const appLocation = path.join(appsDir, `${currentFile.id}`); + eventHandler( + 'file', + ( + name: string, + file: fs.ReadStream, + info: { encoding: string; filename: string; mimeType: string } + ) => { + const { mimeType, filename } = info; + + if ( + mimeType != 'application/x-zip-compressed' && + mimeType != 'application/zip' + ) { + return errorHandler( + new AppError('Please upload a zip file', 404) + ); + } - fs.createReadStream(currentFile.path).pipe( - Extract({ path: appLocation }) + const appLocation = path.join(appsDirectory, deployment.id); + deployment.path = appLocation; + + // Create temporary directory for the blob + fs.mkdtemp( + path.join(os.tmpdir(), `metacall-faas-${deployment.id}-`), + (err, folder) => { + if (err !== null) { + return errorHandler( + new AppError( + 'Failed to create temporary directory for the blob', + 500 + ) + ); + } + + deployment.blob = path.join(folder, filename); + + // Create the app folder + ensureFolderExists(appLocation) + .then(() => { + // Create the write stream for storing the blob + file.pipe( + fs.createWriteStream(deployment.blob as string) + ); + }) + .catch((error: Error) => { + errorHandler( + new AppError( + `Failed to create folder for the deployment at: ${appLocation} - ${error.toString()}`, + 404 + ) + ); + }); + } ); + } + ); + + eventHandler('field', (name: keyof Deployment, val: string) => { + if (name === 'runners') { + deployment['runners'] = JSON.parse(val) as string[]; + } else if (name === 'jsons') { + deployment['jsons'] = JSON.parse(val) as MetaCallJSON[]; + } else { + deployment[name] = val; + } + }); - fs.unlinkSync(currentFile.path); + eventHandler('finish', () => { + if (deployment.blob === undefined) { + throw Error('Invalid file upload, blob path is not defined'); + } + + const deleteBlob = () => { + if (deployment.blob !== undefined) { + fs.unlink(deployment.blob, error => { + if (error !== null) { + errorHandler( + new AppError( + `Failed to delete the blob at: ${error.toString()}`, + 500 + ) + ); + } + }); + } + }; - currentFile.path = appLocation; - }, 'finish'); - }); + const options: ParseOptions = { path: deployment.path }; + + let deployResolve: ( + value: Deployment | PromiseLike + ) => void; + let deployReject: (reason?: unknown) => void; - bb.on('close', () => { - handleError(() => { - res.status(201).json({ - id: currentFile.id + deploymentMap[deployment.id] = new Promise((resolve, reject) => { + deployResolve = resolve; + deployReject = reject; + }); + + fs.createReadStream(deployment.blob) + .pipe(Extract(options)) + .on('close', () => { + deleteBlob(); + deployResolve(deployment); + }) + .on('error', error => { + deleteBlob(); + const appError = new AppError( + `Failed to unzip the deployment at: ${error.toString()}`, + 500 + ); + errorHandler(appError); + deployReject(appError); }); - }, 'close'); + }); + + eventHandler('close', () => { + res.status(201).json({ + id: deployment.id + }); }); req.pipe(bb); diff --git a/src/controller/validate.ts b/src/controller/validate.ts new file mode 100644 index 0000000..f97ae66 --- /dev/null +++ b/src/controller/validate.ts @@ -0,0 +1,7 @@ +import { Request, Response } from 'express'; + +export const validate = (_req: Request, res: Response): Response => + res.status(200).json({ + status: 'success', + data: true + }); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..5395048 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,31 @@ +import colors from 'colors'; +import dotenv from 'dotenv'; + +import app from './app'; +import { findJsonFilesRecursively } from './utils/autoDeploy'; +import { appsDirectory } from './utils/config'; +import { ensureFolderExists } from './utils/utils'; + +// Initialize the FaaS +void (async (): Promise => { + try { + dotenv.config(); + colors.enable(); + + await ensureFolderExists(appsDirectory); + + // TODO: Refactor this + await findJsonFilesRecursively(appsDirectory); + + console.log('Previously deployed applications deployed successfully'); + // END-TODO + + const port = process.env.PORT || 9000; + + app.listen(port, () => { + console.log(`Server is running on the port ${port}`); + }); + } catch (e) { + console.error('Error while re-deploying applications: ', e); + } +})(); diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index 45ef68a..0000000 --- a/src/server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as dotenv from 'dotenv'; - -import app from './app'; - -dotenv.config(); - -const port = process.env.PORT || 9000; - -app.listen(port, () => { - console.log(`Server is running on the port ${port}`); -}); diff --git a/src/utils/autoDeploy.ts b/src/utils/autoDeploy.ts index 75539ec..ba01210 100644 --- a/src/utils/autoDeploy.ts +++ b/src/utils/autoDeploy.ts @@ -1,53 +1,66 @@ +import { pathIsMetaCallJson } from '@metacall/protocol/package'; import { spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { + Deployment, + IAppWithFunctions, + ProtocolMessageType, + WorkerMessage, + WorkerMessageUnknown, allApplications, - childProcessResponse, - cps, - currentFile, - protocol + childProcesses } from '../constants'; -import { isIAllApps } from './utils'; +import { isIAllApps, logProcessOutput } from './utils'; +// TODO: Refactor this export const findJsonFilesRecursively = async ( appsDir: string ): Promise => { + // TODO: Avoid sync commands const files = fs.readdirSync(appsDir, { withFileTypes: true }); for (const file of files) { if (file.isDirectory()) { await findJsonFilesRecursively(path.join(appsDir, file.name)); - } else if (file.name === 'metacall.json') { + } else if (pathIsMetaCallJson(file.name)) { const filePath = path.join(appsDir, file.name); - const desiredPath = path.join(__dirname, '../worker/index.js'); + const desiredPath = path.join( + path.resolve(__dirname, '..'), + 'worker', + 'index.js' + ); const id = path.basename(appsDir); - currentFile.id = id; - (currentFile.type = 'application/x-zip-compressed'), - (currentFile.path = appsDir); + const deployment: Deployment = { + id, + type: 'application/x-zip-compressed', + path: appsDir, + jsons: [] + }; const proc = spawn('metacall', [desiredPath, filePath], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] }); - proc.send({ - type: protocol.l, - currentFile - }); + const message: WorkerMessage = { + type: ProtocolMessageType.Load, + data: deployment + }; - proc.stdout?.on('data', (data: Buffer) => { - console.log(data.toString().green); - }); - proc.stderr?.on('data', (data: Buffer) => { - console.log(data.toString().red); - }); + proc.send(message); + + logProcessOutput(proc.stdout, proc.pid, deployment.id); + logProcessOutput(proc.stderr, proc.pid, deployment.id); - proc.on('message', (data: childProcessResponse) => { - if (data.type === protocol.g) { - if (isIAllApps(data.data)) { - const appName = Object.keys(data.data)[0]; - cps[appName] = proc; - allApplications[appName] = data.data[appName]; + proc.on('message', (payload: WorkerMessageUnknown) => { + if (payload.type === ProtocolMessageType.MetaData) { + const message = payload as WorkerMessage< + Record + >; + if (isIAllApps(message.data)) { + const appName = Object.keys(message.data)[0]; + childProcesses[appName] = proc; + allApplications[appName] = message.data[appName]; } } }); diff --git a/src/utils/config.ts b/src/utils/config.ts index 03bcdc7..bae32f9 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,6 +1,23 @@ +import { platform } from 'os'; import { join } from 'path'; -import { configDir } from './utils'; -export const defaultPath = configDir(join('metacall', 'faas')); +const missing = (name: string): Error => + new Error(`Missing ${name} environment variable! Unable to load config`); -export const appsDirectory = (path = defaultPath): string => join(path, 'apps'); +export const configDir = (name: string): string => { + if (platform() === 'win32') { + if (process.env.APPDATA === undefined) { + throw missing('APPDATA'); + } + return join(process.env.APPDATA, name); + } else { + if (process.env.HOME === undefined) { + throw missing('HOME'); + } + return join(process.env.HOME, `.${name}`); + } +}; + +export const basePath = configDir(join('metacall', 'faas')); + +export const appsDirectory = join(basePath, 'apps'); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 9732acf..29d4e56 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,18 +1,75 @@ import * as fs from 'fs'; import * as path from 'path'; +import { LogMessage } from '../constants'; +import { assignColorToWorker } from './utils'; const logFilePath = path.join(__dirname, '../../logs/'); const logFileName = 'app.log'; const logFileFullPath = path.join(logFilePath, logFileName); -export const logger = { - log: (message: string): void => { +class Logger { + private logQueue: LogMessage[] = []; + private isProcessing = false; + + private async processQueue(): Promise { + if (this.isProcessing || this.logQueue.length === 0) return; + this.isProcessing = true; + + while (this.logQueue.length > 0) { + const logEntry = this.logQueue.shift(); + if (logEntry) { + const { deploymentName, workerPID, message } = logEntry; + this.store(deploymentName, message); + this.present(deploymentName, workerPID, message); + await new Promise(resolve => setTimeout(resolve, 0)); + } + } + + this.isProcessing = false; + } + + public enqueueLog( + deploymentName: string, + workerPID: number, + message: string + ): void { + this.logQueue.push({ deploymentName, workerPID, message }); + this.processQueue().catch(console.error); + } + + private store(deploymentName: string, message: string): void { const timeStamp = new Date().toISOString(); - const logMessage = `${timeStamp} - ${message}\n`; - console.log(message); - if (!fs.existsSync(logFileFullPath)) { - fs.writeFileSync(logFileFullPath, '', { encoding: 'utf-8' }); + const logMessage = `${timeStamp} - ${deploymentName} | ${message}\n`; + + if (!fs.existsSync(logFilePath)) { + fs.mkdirSync(logFilePath, { recursive: true }); } fs.appendFileSync(logFileFullPath, logMessage, { encoding: 'utf-8' }); } -}; + + private present( + deploymentName: string, + workerPID: number, + message: string + ): void { + message = message.trim(); + const fixedWidth = 24; + + let paddedName = deploymentName.padEnd(fixedWidth, ' '); + if (deploymentName.length > fixedWidth) { + paddedName = deploymentName.substring(0, fixedWidth - 2) + '_1'; + } + + // Regular expression for splitting by '\n', '. ', or ' /' + const messageLines = message.split(/(?:\n|\. | \/)/); + const coloredName = assignColorToWorker(`${paddedName} |`, workerPID); + const formattedMessageLines = messageLines.map( + line => `${coloredName} ${line}` + ); + const logMessage = formattedMessageLines.join('\n'); + + console.log(logMessage); + } +} + +export const logger = new Logger(); diff --git a/src/utils/resposeTexts.ts b/src/utils/resposeTexts.ts deleted file mode 100644 index f68ad00..0000000 --- a/src/utils/resposeTexts.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const deleteStatusMessage = ( - app: string -): { - success: string; - error: string; -} => ({ - success: 'Deploy Delete Succeed', - error: `Oops! It looks like the application (${app}) hasn't been deployed yet. Please deploy it before you delete it.` -}); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index e4f2c7e..4183cbd 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,72 +1,66 @@ import { exec } from 'child_process'; -import * as fs from 'fs'; -import { platform } from 'os'; -import { join } from 'path'; +import { promises as fs } from 'fs'; import { LanguageId, MetaCallJSON } from '@metacall/protocol/deployment'; -import { generatePackage, PackageError } from '@metacall/protocol/package'; +import { PackageError, generatePackage } from '@metacall/protocol/package'; import { NextFunction, Request, RequestHandler, Response } from 'express'; import { - createInstallDependenciesScript, - currentFile, + ANSICode, + Deployment, IAllApps, - InspectObject + InspectObject, + PIDToColorCodeMap, + allApplications, + assignedColorCodes, + createInstallDependenciesScript } from '../constants'; import { logger } from './logger'; -export const dirName = (gitUrl: string): string => - String(gitUrl.split('/')[gitUrl.split('/').length - 1]).replace('.git', ''); +export const installDependencies = async ( + deployment: Deployment +): Promise => { + if (!deployment.runners) return; -// Create a proper hashmap that contains all the installation commands mapped to their runner name and shorten this function -export const installDependencies = async (): Promise => { - if (!currentFile.runners) return; - - for (const runner of currentFile.runners) { + for (const runner of deployment.runners) { if (runner == undefined) continue; else { await execPromise( - createInstallDependenciesScript(runner, currentFile.path) + createInstallDependenciesScript(runner, deployment.path) ); } } }; -//check if repo contains metacall-*.json if not create and calculate runners then install dependencies -export const calculatePackages = async (next: NextFunction): Promise => { - const data = await generatePackage(currentFile.path); +// Check if repo contains metacall-*.json if not create and calculate runners then install dependencies +export const calculatePackages = async ( + deployment: Deployment, + next: NextFunction +): Promise => { + const data = await generatePackage(deployment.path); if (data.error == PackageError.Empty) { return next(new Error(PackageError.Empty)); } - // currentFile.jsons = JSON.parse(data.jsons.toString()); FIXME Fix this line - currentFile.runners = data.runners; + // deployment.jsons = JSON.parse(data.jsons.toString()); FIXME Fix this line + deployment.runners = data.runners; }; -export const exists = (path: string): Promise => - fs.promises.stat(path).then( - () => true, - () => false - ); +export const exists = async (path: string): Promise => { + try { + await fs.stat(path); + return true; + } catch (e) { + return false; + } +}; export const ensureFolderExists = async ( path: Path ): Promise => ( - (await exists(path)) || - (await fs.promises.mkdir(path, { recursive: true })), - path + (await exists(path)) || (await fs.mkdir(path, { recursive: true })), path ); -export const deleteRepoFolderIfExist = ( - path: Path, - url: string -): void => { - const folder = dirName(url); - const repoFilePath = join(path, folder); - - fs.rmSync(repoFilePath, { recursive: true, force: true }); -}; - export const execPromise = ( command: string ): Promise<{ @@ -84,39 +78,35 @@ export const execPromise = ( }); export const catchAsync = ( - fn: (req: Request, res: Response, next: NextFunction) => Promise + fn: ( + req: Request, + res: Response, + next: NextFunction + ) => Promise ): RequestHandler => { return (req: Request, res: Response, next: NextFunction) => { - fn(req, res, next).catch(err => next(err)); + return fn(req, res, next).catch(err => next(err)); }; }; -export const createMetacallJsonFile = ( +export const createMetacallJsonFile = async ( jsons: MetaCallJSON[], path: string -): string[] => { +): Promise => { const acc: string[] = []; - jsons.forEach(el => { + for (const el of jsons) { const filePath = `${path}/metacall-${el.language_id}.json`; + try { + await fs.writeFile(filePath, JSON.stringify(el)); + acc.push(filePath); + } catch (e) { + // TODO: Do something here? + } + } - fs.writeFileSync(filePath, JSON.stringify(el)); - acc.push(filePath); - }); return acc; }; -const missing = (name: string): string => - `Missing ${name} environment variable! Unable to load config`; - -export const configDir = (name: string): string => - platform() === 'win32' - ? process.env.APPDATA - ? join(process.env.APPDATA, name) - : missing('APPDATA') - : process.env.HOME - ? join(process.env.HOME, `.${name}`) - : missing('HOME'); - export const getLangId = (input: string): LanguageId => { const parts = input.split('-'); const extension = parts[parts.length - 1].split('.')[0]; @@ -143,9 +133,37 @@ export function isIAllApps(data: unknown): data is IAllApps { export function logProcessOutput( proc: NodeJS.ReadableStream | null, - color: 'green' | 'red' + workerPID: number | undefined, + deploymentName: string ): void { proc?.on('data', (data: Buffer) => { - logger.log(data.toString()[color]); + logger.enqueueLog(deploymentName, workerPID || 0, data.toString()); }); } + +export const maxWorkerWidth = (maxIndexWidth = 3): number => { + const workerLengths = Object.keys(allApplications).map( + worker => worker.length + ); + return Math.max(...workerLengths) + maxIndexWidth; +}; + +export const assignColorToWorker = ( + deploymentName: string, + workerPID: number +): string => { + if (!PIDToColorCodeMap[workerPID]) { + let colorCode: number; + + // Keep looking for unique code + do { + colorCode = ANSICode[Math.floor(Math.random() * ANSICode.length)]; + } while (assignedColorCodes[colorCode]); + + // Assign the unique code and mark it as used + PIDToColorCodeMap[workerPID] = colorCode; + assignedColorCodes[colorCode] = true; + } + const assignColorCode = PIDToColorCodeMap[workerPID]; + return `\x1b[38;5;${assignColorCode}m${deploymentName}\x1b[0m`; +}; diff --git a/src/worker/index.ts b/src/worker/index.ts index 2b075dc..e996a61 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -8,14 +8,16 @@ import { import { hostname } from 'os'; import { App, + Deployment, IAppWithFunctions, - currentUploadedFile, - protocol + ProtocolMessageType, + WorkerMessage, + WorkerMessageUnknown } from '../constants'; import { createMetacallJsonFile, diff } from '../utils/utils'; -let currentFile: currentUploadedFile = { +let deployment: Deployment = { id: '', type: '', jsons: [], @@ -65,7 +67,7 @@ const handleNoJSONFile = ( if (process.send) { process.send({ - type: protocol.g, + type: ProtocolMessageType.MetaData, data: allApplications }); } @@ -78,25 +80,33 @@ const handleJSONFiles = async ( suffix: string, version: string ): Promise => { - let jsonPath: string[] = - currentFile.jsons.length > 0 - ? createMetacallJsonFile(currentFile.jsons, path) - : findMetaCallJsons(await findFilesPath(path)).map( - el => `${path}/${el}` - ); - // FIXME Currently it do not support metacall.json syntax, else metacall-{runner}.json is fine and will work - handleNoJSONFile(jsonPath, suffix, version); + if (deployment.jsons.length > 0) { + const jsonPaths = await createMetacallJsonFile(deployment.jsons, path); + handleNoJSONFile(jsonPaths, suffix, version); + } else { + const filesPaths = await findFilesPath(path); + const jsonPaths = findMetaCallJsons(filesPaths).map( + el => `${path}/${el}` + ); + handleNoJSONFile(jsonPaths, suffix, version); + } }; -process.on('message', payload => { - if (payload.type === protocol.l) { - currentFile = payload.currentFile; - handleJSONFiles(currentFile.path, currentFile.id, 'v1'); - } else if (payload.type === protocol.c) { +process.on('message', (payload: WorkerMessageUnknown) => { + if (payload.type === ProtocolMessageType.Load) { + deployment = (payload as WorkerMessage).data; + handleJSONFiles(deployment.path, deployment.id, 'v1'); + } else if (payload.type === ProtocolMessageType.Invoke) { + const fn = ( + payload as WorkerMessage<{ + name: string; + args: unknown[]; + }> + ).data; if (process.send) { process.send({ - type: protocol.r, - data: exactFnx[payload.fn.name](...payload.fn.args) + type: ProtocolMessageType.InvokeResult, + data: exactFnx[fn.name](...fn.args) }); } } diff --git a/test/data/python-base-app/index.py b/test/data/python-base-app/index.py new file mode 100644 index 0000000..b9db4f0 --- /dev/null +++ b/test/data/python-base-app/index.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python + +def number(): + return 100 + +def text(): + return 'asd' diff --git a/test/data/python-base-app/metacall.json b/test/data/python-base-app/metacall.json new file mode 100644 index 0000000..a8a59b1 --- /dev/null +++ b/test/data/python-base-app/metacall.json @@ -0,0 +1,7 @@ +{ + "language_id": "py", + "path": ".", + "scripts": [ + "index.py" + ] +} diff --git a/test/test.sh b/test/test.sh new file mode 100644 index 0000000..5a42dc4 --- /dev/null +++ b/test/test.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# +# MetaCall FaaS Script by Parra Studios +# Reimplementation of MetaCall FaaS platform written in TypeScript. +# +# Copyright (C) 2016 - 2024 Vicente Eduardo Ferrer Garcia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -exuo pipefail + +# FaaS base URL +BASE_URL="http://localhost:9000" + +# Get the prefix of a deployment +function getPrefix() { + prefix=$(metacall-deploy --dev --inspect Raw | jq -r ".[] | select(.suffix == \"$1\") | .prefix") + echo $prefix +} + +# Wait for the FaaS to be ready +while [[ ! $(curl -s -o /dev/null -w "%{http_code}" $BASE_URL/readiness) = "200" ]]; do + sleep 1 +done + +echo "FaaS ready, starting tests." + +# Test deploy (Python) without dependencies +app="python-base-app" +pushd data/$app + metacall-deploy --dev + prefix=$(getPrefix $app) + url=$BASE_URL/$prefix/$app/v1/call + [[ $(curl -s $url/number) = 100 ]] || exit 1 + [[ $(curl -s $url/text) = '"asd"' ]] || exit 1 +popd diff --git a/types/metacall.d.ts b/types/metacall.d.ts index c6808f8..6399653 100755 --- a/types/metacall.d.ts +++ b/types/metacall.d.ts @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable @typescript-eslint/no-explicit-any */ declare module 'metacall' { export function metacall(name: string, ...args: any): any;